@eleven-am/nestjs-storage
Version:
A NestJS module for uploading files to cloud storage providers
280 lines (260 loc) • 7.89 kB
text/typescript
import { BaseStorage } from './baseStorage';
import { drive_v3, google } from 'googleapis';
import { GDriveStorageOption } from '../types/options';
import { IFile, PartialStream } from '../types/storage';
export class GDriveStorage extends BaseStorage {
private readonly drive: drive_v3.Drive;
constructor(options: GDriveStorageOption) {
super(options.provider);
const auth = new google.auth.OAuth2(
options.options.clientId,
options.options.clientSecret,
);
auth.setCredentials({
refresh_token: options.options.refreshToken,
});
this.drive = google.drive({ version: 'v3', auth: auth });
}
createFolder(path: string) {
return new Promise<IFile>((resolve, reject) => {
this.drive.files
.create(
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource: {
name: path,
mimeType: 'application/vnd.google-apps.folder',
},
fields:
'id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed',
},
(err: unknown, data: { data: drive_v3.Schema$File }) => {
if (err) {
reject(err);
} else {
resolve(this.parseFile(data.data));
}
},
)
.catch(reject);
});
}
deleteFileOrFolder(fileId: string) {
return new Promise<boolean>((resolve, reject) => {
this.drive.files.delete(
{
fileId: fileId,
supportsAllDrives: true,
},
(err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
},
);
});
}
getFileOrFolder(fileId: string) {
return new Promise<IFile>((resolve, reject) => {
this.drive.files.get(
{
fileId: fileId,
supportsAllDrives: true,
fields:
'id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed',
},
(err, data) => {
if (err) {
reject(err);
} else if (data?.data) {
resolve(this.parseFile(data.data));
} else {
reject(new Error('File not found'));
}
},
);
});
}
moveFileOrFolder(fileId: string, newPath: string) {
return new Promise<IFile>((resolve, reject) => {
this.drive.files.update(
{
fileId: fileId,
supportsAllDrives: true,
addParents: newPath,
removeParents: 'root',
fields:
'id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed',
},
(err, data) => {
if (err) {
reject(err);
} else if (data?.data) {
resolve(this.parseFile(data.data));
} else {
reject(new Error('File not found'));
}
},
);
});
}
putFile(path: string, data: Buffer) {
return new Promise<IFile>((resolve, reject) => {
this.drive.files
.create(
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
resource: {
name: path,
parents: ['root'],
},
media: {
mimeType: 'application/octet-stream',
body: data,
},
fields:
'id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed',
},
(err: unknown, data: { data: drive_v3.Schema$File }) => {
if (err) {
reject(err);
} else {
resolve(this.parseFile(data.data));
}
},
)
.catch(reject);
});
}
readFile(fileId: string) {
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
this.drive.files
.get(
{
fileId: fileId,
alt: 'media',
supportsAllDrives: true,
},
{
responseType: 'stream',
},
)
.then((res): NodeJS.ReadableStream => res.data)
.catch(reject);
});
}
readFolder(folderId: string) {
return this.readFolderRecursive(folderId);
}
renameFileOrFolder(fileId: string, newName: string) {
return new Promise<IFile>((resolve, reject) => {
this.drive.files
.update({
fileId: fileId,
requestBody: {
name: newName,
},
fields:
'id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed',
})
.then((res) => resolve(this.parseFile(res.data)))
.catch(reject);
});
}
getSignedUrl(fileId: string) {
return new Promise<string>((resolve, reject) => {
this.drive.files
.get({
fileId: fileId,
supportsAllDrives: true,
fields: 'id, name, size, parents, modifiedTime, mimeType',
})
.then((res) => resolve(res.data.webContentLink || ''))
.catch(reject);
});
}
streamFile(fileId: string, range: string) {
return new Promise<PartialStream>((resolve, reject) => {
this.drive.files
.get(
{
fileId: fileId,
alt: 'media',
supportsAllDrives: true,
},
{
responseType: 'stream',
headers: {
Range: range,
},
},
)
.then((res) => {
resolve({
stream: res.data,
headers: {
contentLength: res.headers['content-length'],
contentType: res.headers['content-type'],
contentRange: res.headers['content-range'],
contentDisposition: res.headers['content-disposition'],
},
});
})
.catch(reject);
});
}
readRootFolder() {
return this.readFolder('root');
}
private parseFile(file: drive_v3.Schema$File): IFile {
if (file.id === undefined) {
throw new Error('File id is undefined');
}
if (file.name === undefined) {
throw new Error('File name is undefined');
}
return {
name: file.name || '',
path: file.id || '',
size: Number(file.size) || 0,
mimeType: file.mimeType || null,
isFolder: file.mimeType === 'application/vnd.google-apps.folder',
modifiedAt: new Date(file.modifiedTime || 0),
};
}
private readFolderRecursive(folderId: string, pageToken?: string) {
return new Promise<IFile[]>((resolve, reject) => {
this.drive.files.list(
{
q: `'${folderId}' in parents and trashed = false`,
fields:
'nextPageToken, files(id, name, size, parents, modifiedTime, mimeType, contentHints/thumbnail, videoMediaMetadata, thumbnailLink, explicitlyTrashed)',
supportsAllDrives: true,
pageToken: pageToken,
spaces: 'drive',
orderBy: 'name',
pageSize: 1000,
},
(err, data) => {
if (err) {
reject(err);
} else {
const files =
data?.data.files?.map((file) => this.parseFile(file)) || [];
if (data?.data.nextPageToken) {
this.readFolderRecursive(folderId, data.data.nextPageToken)
.then((nextFiles) => resolve([...files, ...nextFiles]))
.catch(reject);
} else {
resolve(files);
}
}
},
);
});
}
}