UNPKG

@eleven-am/nestjs-storage

Version:

A NestJS module for uploading files to cloud storage providers

316 lines (277 loc) 8.31 kB
import { BaseStorage } from './baseStorage'; import { IFile, PartialStream } from '../types/storage'; import { z } from 'zod'; import { getMimeType } from '../lib/getMimetype'; import { makeRequest } from '../lib/makeRequest'; import { DropboxStorageOption } from '../types/options'; const dropboxFileSchema = z.object({ '.tag': z.union([z.literal('file'), z.literal('folder')]), name: z.string(), id: z.string(), path_lower: z.string(), path_display: z.string(), client_modified: z.string(), size: z.number().optional(), }); const dropboxGetFilesResponseSchema = z.object({ entries: z.array(dropboxFileSchema), has_more: z.boolean(), cursor: z.string(), }); const dropboxTokenSchema = z.object({ access_token: z.string(), expires_in: z.number(), refresh_token: z.string(), }); type DropBoxFile = z.infer<typeof dropboxFileSchema>; type DropBoxToken = z.infer<typeof dropboxTokenSchema>; interface DropBoxCredentials { clientId: string; clientSecret: string; } export class DropboxStorage extends BaseStorage { private readonly credentials: DropBoxCredentials; private readonly refreshToken: string; private token: DropBoxToken | null; constructor(options: DropboxStorageOption) { super(options.provider); this.credentials = { clientId: options.options.clientId, clientSecret: options.options.clientSecret, }; this.refreshToken = options.options.refreshToken; } async createFolder(path: string) { const params = { path: 'https://api.dropboxapi.com/2/files/create_folder_v2', query: { path: `${path}`, }, }; const data = await this.makeRequest(params, dropboxFileSchema); return this.parseFile(data); } async deleteFileOrFolder(fileId: string) { const params = { path: 'https://api.dropboxapi.com/2/files/delete_v2', query: { path: `${fileId}`, }, }; await this.makeRequest(params, dropboxFileSchema); return true; } async getFileOrFolder(fileId: string) { const params = { path: 'https://api.dropboxapi.com/2/files/get_metadata', query: { path: `${fileId}`, }, }; const data = await this.makeRequest(params, dropboxFileSchema); return this.parseFile(data); } async moveFileOrFolder(fileId: string, newPath: string) { const params = { path: 'https://api.dropboxapi.com/2/files/move_v2', query: { from_path: fileId, to_path: newPath, }, }; const data = await this.makeRequest(params, dropboxFileSchema); return this.parseFile(data); } async putFile(path: string, data: Buffer) { const token = await this.authenticate(); const response = await fetch( 'https://content.dropboxapi.com/2/files/upload', { method: 'POST', headers: { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/octet-stream', 'Dropbox-API-Arg': JSON.stringify({ path, mode: 'overwrite', }), }, body: data, }, ); try { const json = await response.json(); const dropboxFile = dropboxFileSchema.parse(json); return this.parseFile(dropboxFile); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error('Failed to upload file to Dropbox'); } } async readFile(fileId: string) { const token = await this.authenticate(); const options = { method: 'POST', headers: { authorization: `Bearer ${token.access_token}`, 'Dropbox-API-Arg': JSON.stringify({ path: fileId, }), 'Content-Type': 'application/octet-stream', }, }; return new Promise<NodeJS.ReadableStream>((resolve, reject) => { fetch('https://content.dropboxapi.com/2/files/download', options) .then((response) => { if (!response.ok) { reject(response); } resolve(response.body as unknown as NodeJS.ReadableStream); }) .catch((error) => { reject(error); }); }); } async readFolder(folderId: string) { const query = { path: folderId, include_media_info: true, include_deleted: false, include_has_explicit_shared_members: false, include_mounted_folders: true, include_non_downloadable_files: true, }; const params = { path: 'https://api.dropboxapi.com/2/files/list_folder', query, }; const data = await this.makeRequest(params, dropboxGetFilesResponseSchema); return data.entries.map((file) => this.parseFile(file)); } readRootFolder(): Promise<IFile[]> { return this.readFolder(''); } renameFileOrFolder(fileId: string, newName: string) { return this.moveFileOrFolder(fileId, newName); } getSignedUrl(fileId: string) { const params = { path: 'https://api.dropboxapi.com/2/files/get_temporary_link', query: { path: fileId, }, }; return new Promise<string>((resolve, reject) => { this.makeRequest( params, z.object({ link: z.string(), metadata: dropboxFileSchema, }), ) .then((data) => { resolve(data.link); }) .catch((error) => { reject(error); }); }); } async streamFile(fileId: string, range: string) { const token = await this.authenticate(); const options = { method: 'POST', headers: { authorization: `Bearer ${token.access_token}`, 'Dropbox-API-Arg': JSON.stringify({ path: fileId, }), 'Content-Type': 'application/octet-stream', Range: range, }, }; return new Promise<PartialStream>((resolve, reject) => { fetch('https://content.dropboxapi.com/2/files/download', options) .then((response) => { if (!response.ok) { reject(response); } resolve({ stream: response.body as unknown as NodeJS.ReadableStream, headers: { contentType: response.headers.get('Content-Type') || '', contentLength: response.headers.get('Content-Length') || '', contentRange: response.headers.get('Content-Range') || '', contentDisposition: response.headers.get('Content-Disposition') || '', }, }); }) .catch((error) => { reject(error); }); }); } private parseFile(file: DropBoxFile): IFile { return { name: file.name, path: file.path_lower, size: file.size || 0, mimeType: getMimeType(file.name), isFolder: file['.tag'] === 'folder', modifiedAt: new Date(file.client_modified), }; } private makeRequest<DataType>( { path, query }: { path: string; query: Record<string, unknown> }, schema: z.ZodType<DataType>, ) { return new Promise<DataType>((resolve, reject) => { this.authenticate() .then((token) => { const params = { address: `https://api.dropboxapi.com/2${path}`, method: 'POST' as const, headers: { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(query), }; return makeRequest(params, schema); }) .then((data) => { resolve(data); }) .catch((error) => { reject(error); }); }); } private async authenticate() { if (this.token && this.token.expires_in > Date.now()) { return this.token; } const query = { grant_type: 'refresh_token', refresh_token: this.refreshToken, client_id: this.credentials.clientId, client_secret: this.credentials.clientSecret, }; const response = await makeRequest( { address: 'https://api.dropboxapi.com/oauth2/token', method: 'GET', query, }, dropboxTokenSchema, ); const token = { ...response, expires_in: Date.now() + response.expires_in * 1000, }; this.token = token; return token; } }