Skip to content

Commit f05ca6a

Browse files
committed
feat: add lib storage provider
1 parent 8718eda commit f05ca6a

6 files changed

Lines changed: 463 additions & 0 deletions

File tree

src/lib/date.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function ms(value: string): number {
2+
const TIME_UNITS = {
3+
s: 1000,
4+
m: 60 * 1000,
5+
h: 60 * 60 * 1000,
6+
d: 24 * 60 * 60 * 1000,
7+
w: 7 * 24 * 60 * 60 * 1000,
8+
}
9+
10+
const type = value.slice(-1)
11+
const numericValue = parseInt(value.slice(0, -1), 10)
12+
13+
if (isNaN(numericValue) || !(type in TIME_UNITS)) {
14+
throw new Error('Invalid time format')
15+
}
16+
17+
// @ts-expect-error
18+
return numericValue * TIME_UNITS[type]
19+
}

src/lib/storage/gcs.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as GCS from '@google-cloud/storage'
2+
import { green } from 'colorette'
3+
import { addDays } from 'date-fns'
4+
import fs from 'fs'
5+
import path from 'path'
6+
import { logger } from '~/config/logger'
7+
import { __dirname } from '~/lib/string'
8+
import { ms } from '../date'
9+
import { GoogleCloudStorageParams, UploadFileParams } from './types'
10+
11+
export default class GoogleCloudStorage {
12+
public client: GCS.Storage
13+
14+
private _access_key: string
15+
private _filepath: string
16+
private _bucket: string
17+
private _expires: string
18+
19+
constructor(params: GoogleCloudStorageParams) {
20+
this._access_key = params.access_key
21+
this._bucket = params.bucket
22+
this._expires = params.expires
23+
this._filepath = path.resolve(`${__dirname}/${params.filepath}`)
24+
25+
const msgType = `${green('storage - google cloud storage')}`
26+
27+
if (!this._access_key && !fs.existsSync(this._filepath)) {
28+
const message = `${msgType} serviceAccount is missing on root directory`
29+
logger.error(message)
30+
31+
throw new Error(
32+
'Missing GCP Service Account!!!\nCopy gcp-serviceAccount from your console google to root directory "gcp-serviceAccount.json"'
33+
)
34+
}
35+
36+
if (this._access_key) {
37+
const message = `${msgType} - ${this._filepath}`
38+
logger.info(message)
39+
}
40+
41+
this.client = new GCS.Storage({
42+
projectId: this._access_key,
43+
keyFilename: this._filepath,
44+
})
45+
}
46+
47+
private _generateKeyfile(values: string[]) {
48+
return values.join('/')
49+
}
50+
51+
public expiresObject() {
52+
const getExpired = this._expires.replace(/[^0-9]/g, '')
53+
54+
const expiresIn = ms(this._expires)
55+
const expiryDate = addDays(new Date(), Number(getExpired))
56+
57+
return { expiresIn, expiryDate }
58+
}
59+
60+
async initialize() {
61+
const msgType = `${green('storage - google cloud storage')}`
62+
const bucketName = this._bucket
63+
64+
try {
65+
const data = this.client.bucket(bucketName)
66+
const getBucket = await data.exists()
67+
const getMetadata = await data.getMetadata()
68+
69+
if (getBucket[0]) {
70+
const message = `${msgType} - ${bucketName} bucket found`
71+
logger.info(message)
72+
console.log(getMetadata[0])
73+
}
74+
} catch (error) {
75+
const message = `${msgType} - ${bucketName} bucket not found`
76+
logger.error(message)
77+
// create bucket if not exists
78+
await this._createBucket()
79+
}
80+
}
81+
82+
private async _createBucket() {
83+
const msgType = `${green('storage - google cloud storage')}`
84+
const bucketName = this._bucket
85+
86+
try {
87+
const data = await this.client.createBucket(bucketName)
88+
const getMetadata = await data[0].getMetadata()
89+
90+
const message = `${msgType} - ${bucketName} bucket created`
91+
logger.info(message)
92+
console.log(getMetadata[0])
93+
} catch (error: any) {
94+
const message = `${msgType} error: ${error.message ?? error}`
95+
logger.error(message)
96+
process.exit(1)
97+
}
98+
}
99+
100+
async uploadFile({ directory, file }: UploadFileParams) {
101+
const keyfile = this._generateKeyfile([directory, file.filename])
102+
103+
// For a destination object that does not yet exist,
104+
// set the ifGenerationMatch precondition to 0
105+
// If the destination object already exists in your bucket, set instead a
106+
// generation-match precondition using its generation number.
107+
const generationMatchPrecondition = 0
108+
109+
const options: GCS.UploadOptions = {
110+
destination: keyfile,
111+
preconditionOpts: { ifGenerationMatch: generationMatchPrecondition },
112+
}
113+
114+
const data = await this.client.bucket(this._bucket).upload(file.path, options)
115+
const signedUrl = await this.presignedURL(keyfile)
116+
117+
return { data: data[1], signedUrl }
118+
}
119+
120+
async presignedURL(keyfile: string) {
121+
const msgType = `${green('storage - google cloud storage')}`
122+
const bucketName = this._bucket
123+
124+
const { expiresIn } = this.expiresObject()
125+
const options: GCS.GetSignedUrlConfig = {
126+
version: 'v4',
127+
action: 'read',
128+
virtualHostedStyle: true,
129+
expires: Date.now() + expiresIn,
130+
}
131+
132+
const data = await this.client.bucket(bucketName).file(keyfile).getSignedUrl(options)
133+
const signedUrl = data[0]
134+
135+
const message = `${msgType} - ${keyfile} presigned URL generated`
136+
logger.info(message)
137+
138+
return signedUrl
139+
}
140+
}

src/lib/storage/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import GoogleCloudStorage from './gcs'
2+
import MinIOStorage from './minio'
3+
import S3Storage from './s3'
4+
import { GoogleCloudStorageParams, MinIOStorageParams, S3StorageParams } from './types'
5+
6+
export type StorageType = 's3' | 'minio' | 'gcs'
7+
8+
export type StorageParams = {
9+
storageType: StorageType
10+
params: S3StorageParams | MinIOStorageParams | GoogleCloudStorageParams
11+
}
12+
13+
export default class Storage {
14+
constructor({ storageType, params }: StorageParams) {
15+
switch (storageType) {
16+
case 's3':
17+
// @ts-expect-error
18+
return new S3Storage(params)
19+
20+
case 'minio':
21+
// @ts-expect-error
22+
return new MinIOStorage(params)
23+
24+
case 'gcs':
25+
// @ts-expect-error
26+
return new GoogleCloudStorage(params)
27+
28+
default:
29+
throw new Error('Invalid storage type')
30+
}
31+
}
32+
}

src/lib/storage/minio.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { green } from 'colorette'
2+
import { addDays } from 'date-fns'
3+
import * as Minio from 'minio'
4+
import { logger } from '~/config/logger'
5+
import { ms } from '../date'
6+
import { MinIOStorageParams, UploadFileParams } from './types'
7+
8+
export default class MinIOStorage {
9+
public client: Minio.Client
10+
private _access_key: string
11+
private _secret_key: string
12+
private _bucket: string
13+
private _expires: string
14+
private _region: string
15+
private _host: string
16+
private _port: number
17+
private _ssl: boolean
18+
19+
constructor(params: MinIOStorageParams) {
20+
this._access_key = params.access_key
21+
this._secret_key = params.secret_key
22+
this._bucket = params.bucket
23+
this._expires = params.expires
24+
this._region = params.region
25+
this._host = params.host
26+
this._port = params.port
27+
this._ssl = params.ssl
28+
29+
this.client = new Minio.Client({
30+
endPoint: this._host || '127.0.0.1',
31+
port: this._port || 9000,
32+
useSSL: this._ssl || false,
33+
accessKey: this._access_key,
34+
secretKey: this._secret_key,
35+
})
36+
}
37+
38+
private _generateKeyfile(values: string[]) {
39+
return values.join('/')
40+
}
41+
42+
public expiresObject() {
43+
const getExpired = this._expires.replace(/[^0-9]/g, '')
44+
45+
const expiresIn = ms(this._expires)
46+
const expiryDate = addDays(new Date(), Number(getExpired))
47+
48+
return { expiresIn, expiryDate }
49+
}
50+
51+
async initialize() {
52+
const msgType = `${green('storage - minio')}`
53+
const bucketName = this._bucket
54+
55+
const exists = await this.client.bucketExists(bucketName)
56+
57+
if (!exists) {
58+
await this._createBucket()
59+
} else {
60+
const message = `${msgType} - ${bucketName} bucket found`
61+
logger.info(message)
62+
}
63+
}
64+
65+
private async _createBucket() {
66+
const msgType = `${green('storage - minio')}`
67+
const bucketName = this._bucket
68+
69+
try {
70+
const data = await this.client.makeBucket(bucketName, this._region)
71+
72+
const message = `${msgType} - ${bucketName} bucket created`
73+
logger.info(message)
74+
console.log(data)
75+
} catch (error: any) {
76+
const message = `${msgType} error: ${error.message ?? error}`
77+
logger.error(message)
78+
process.exit(1)
79+
}
80+
}
81+
82+
async uploadFile({ directory, file }: UploadFileParams) {
83+
const keyfile = this._generateKeyfile([directory, file.filename])
84+
85+
const options = {
86+
ContentType: file.mimetype, // <-- this is what you need!
87+
ContentDisposition: `inline; filename=${file.filename}`, // <-- and this !
88+
ACL: 'public-read', // <-- this makes it public so people can see it
89+
}
90+
91+
const data = await this.client.fPutObject(this._bucket, keyfile, file.path, options)
92+
const signedUrl = await this.presignedURL(keyfile)
93+
94+
return { data, signedUrl }
95+
}
96+
97+
async presignedURL(keyfile: string) {
98+
const msgType = `${green('storage - minio')}`
99+
const bucketName = this._bucket
100+
101+
const signedUrl = await this.client.presignedGetObject(bucketName, keyfile)
102+
103+
const message = `${msgType} - ${keyfile} presigned URL generated`
104+
logger.info(message)
105+
106+
return signedUrl
107+
}
108+
}

0 commit comments

Comments
 (0)