Skip to content

Commit

Permalink
fix: implement cloud file operations
Browse files Browse the repository at this point in the history
  • Loading branch information
kukhariev committed Sep 3, 2021
1 parent 22c2955 commit 7023397
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 16 deletions.
8 changes: 4 additions & 4 deletions examples/express-gcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const app = express();

const storage = new GCStorage({ maxUploadSize: '1GB' });

storage.onComplete = ({ uri, id }) => {
console.log(`File upload complete, storage path: ${uri}`);
// send gcs link to client
return { id, link: uri };
storage.onComplete = async file => {
const info = await file.get().catch(console.error);
console.log(info);
return file.move(file.originalName).catch(console.error);
};

app.use('/files', uploadx({ storage }));
Expand Down
80 changes: 71 additions & 9 deletions packages/gcs/src/gcs-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as http from 'http';
import request from 'node-fetch';
import { authScopes, BUCKET_NAME, storageAPI, uploadAPI } from './constants';
import { GCSMetaStorage, GCSMetaStorageOptions } from './gcs-meta-storage';
import { resolve } from 'url';

export interface ClientError extends Error {
code: string;
Expand Down Expand Up @@ -72,9 +73,13 @@ export interface GCStorageOptions extends BaseStorageOptions<GCSFile>, GoogleAut
metaStorageConfig?: LocalMetaStorageOptions | GCSMetaStorageOptions;
}

export class GCSFile extends File {
export interface GCSFile extends File {
GCSUploadURI?: string;
uri = '';
uri: string;
move: (dest: string) => Promise<Record<string, string>>;
copy: (dest: string) => Promise<Record<string, string>>;
get: () => Promise<Record<string, string>>;
delete: () => Promise<any>;
}

/**
Expand All @@ -95,6 +100,7 @@ export class GCStorage extends BaseStorage<GCSFile> {
storageBaseURI: string;
uploadBaseURI: string;
meta: MetaStorage<GCSFile>;
private readonly bucket: string;

constructor(public config: GCStorageOptions = {}) {
super(config);
Expand All @@ -109,11 +115,11 @@ export class GCStorage extends BaseStorage<GCSFile> {
}
config.scopes ||= authScopes;
config.keyFile ||= process.env.GCS_KEYFILE;
const bucketName = config.bucket || process.env.GCS_BUCKET || BUCKET_NAME;
this.storageBaseURI = [storageAPI, bucketName, 'o'].join('/');
this.uploadBaseURI = [uploadAPI, bucketName, 'o'].join('/');
this.bucket = config.bucket || process.env.GCS_BUCKET || BUCKET_NAME;
this.storageBaseURI = [storageAPI, this.bucket, 'o'].join('/');
this.uploadBaseURI = [uploadAPI, this.bucket, 'o'].join('/');
this.authClient = new GoogleAuth(config);
this._checkBucket(bucketName);
this._checkBucket();
}

normalizeError(error: ClientError): HttpError {
Expand All @@ -131,7 +137,7 @@ export class GCStorage extends BaseStorage<GCSFile> {
}

async create(req: http.IncomingMessage, config: FileInit): Promise<GCSFile> {
const file = new GCSFile(config);
const file = new File(config) as GCSFile;
file.name = this.namingFunction(file);
await this.validate(file);
try {
Expand Down Expand Up @@ -173,6 +179,7 @@ export class GCStorage extends BaseStorage<GCSFile> {
if (isCompleted(file)) {
file.uri = `${this.storageBaseURI}/${file.name}`;
await this._onComplete(file);
return this.buildCompletedFile(file);
}
return file;
}
Expand All @@ -190,6 +197,61 @@ export class GCStorage extends BaseStorage<GCSFile> {
return [{ name } as GCSFile];
}

async copy(name: string, dest: string): Promise<Record<string, string>> {
type CopyProgress = {
rewriteToken?: string;
kind: string;
objectSize: number;
totalBytesRewritten: number;
done: boolean;
resource: Record<string, any>;
};
const newPath = resolve(`/${this.bucket}/${name}`, encodeURI(dest));
const [, bucket, ...pathSegments] = newPath.split('/');
const filename = pathSegments.join('/');
const url = `${this.storageBaseURI}/${name}/rewriteTo/b/${bucket}/o/${filename}`;
let progress = {} as CopyProgress;
const opts = {
body: '',
headers: { 'Content-Type': 'application/json' },
method: 'POST' as const,
url
};
do {
opts.body = progress.rewriteToken
? JSON.stringify({ rewriteToken: progress.rewriteToken })
: '';
progress = (await this.authClient.request<CopyProgress>(opts)).data;
} while (progress.rewriteToken);
return progress.resource;
}

async move(name: string, dest: string): Promise<Record<string, string>> {
const resource = await this.copy(name, dest);
const url = `${this.storageBaseURI}/${name}`;
await this.authClient.request({ method: 'DELETE' as const, url });
return resource;
}

async _get(name: string): Promise<Record<string, string>> {
const url = `${this.storageBaseURI}/${name}`;
return (await this.authClient.request<Record<string, string>>({ url })).data;
}

buildCompletedFile(file: GCSFile): GCSFile {
const completed = { ...file };
completed.lock = async lockFn => {
completed.lockedBy = lockFn;
return Promise.resolve(completed.lockedBy);
};
completed.get = () => this._get(file.name);
completed.delete = () => this.delete(file.name);
completed.copy = async (dest: string) => this.copy(file.name, dest);
completed.move = async (dest: string) => this.move(file.name, dest);

return completed;
}

protected async _write(part: FilePart & GCSFile): Promise<number> {
const { size, uri, body } = part;
const contentRange = buildContentRange(part);
Expand Down Expand Up @@ -228,9 +290,9 @@ export class GCStorage extends BaseStorage<GCSFile> {
return this.deleteMeta(file.name);
};

private _checkBucket(bucketName: string): void {
private _checkBucket(): void {
this.authClient
.request({ url: `${storageAPI}/${bucketName}` })
.request({ url: this.storageBaseURI })
.then(() => (this.isReady = true))
.catch((err: ClientError) => {
// eslint-disable-next-line no-console
Expand Down
40 changes: 38 additions & 2 deletions packages/s3/src/s3-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CompleteMultipartUploadOutput,
CopyObjectCommand,
CopyObjectCommandInput,
CopyObjectCommandOutput,
CreateMultipartUploadCommand,
CreateMultipartUploadRequest,
DeleteBucketCommand,
DeleteObjectCommandInput,
DeleteObjectCommandOutput,
HeadBucketCommand,
ListMultipartUploadsCommand,
ListPartsCommand,
Expand Down Expand Up @@ -34,6 +40,7 @@ import {
import * as http from 'http';
import { AWSError } from './aws-error';
import { S3MetaStorage, S3MetaStorageOptions } from './s3-meta-storage';
import { resolve } from 'url';

const BUCKET_NAME = 'node-uploadx';

Expand All @@ -42,8 +49,8 @@ export interface S3File extends File {
UploadId?: string;
uri?: string;
lock: (lockFn: () => any) => Promise<any>;
move: (dest: any) => Promise<any>;
copy: (dest: any) => Promise<any>;
move: (dest: string) => Promise<Record<string, any>>;
copy: (dest: string) => Promise<Record<string, any>>;
delete: () => Promise<any>;
}

Expand Down Expand Up @@ -183,6 +190,7 @@ export class S3Storage extends BaseStorage<S3File> {
const [completed] = await this._onComplete(file);
delete file.Parts;
file.uri = completed.Location;
return this.buildCompletedFile(file);
}
return file;
}
Expand All @@ -197,6 +205,34 @@ export class S3Storage extends BaseStorage<S3File> {
return [{ name } as S3File];
}

async copy(name: string, dest: string): Promise<CopyObjectCommandOutput> {
const CopySource = encodeURI(`${this.bucket}/${name}`);
const newPath = resolve(`/${CopySource}`, encodeURI(dest));
const [, Bucket, ...pathSegments] = newPath.split('/');
const Key = pathSegments.join('/');
const params: CopyObjectCommandInput = { Bucket, Key, CopySource };
return this.client.send(new CopyObjectCommand(params));
}

async move(name: string, dest: string): Promise<DeleteObjectCommandOutput> {
await this.copy(name, dest);
const params: DeleteObjectCommandInput = { Bucket: this.bucket, Key: name };
return this.client.send(new DeleteBucketCommand(params));
}

buildCompletedFile(file: S3File): S3File {
const completed = { ...file };
completed.lock = async lockFn => {
completed.lockedBy = lockFn;
return Promise.resolve(completed.lockedBy);
};
completed.delete = () => this.delete(file.name);
completed.copy = async (dest: string) => this.copy(file.name, dest);
completed.move = async (dest: string) => this.move(file.name, dest);

return completed;
}

protected _onComplete = (file: S3File): Promise<[CompleteMultipartUploadOutput, any]> => {
return Promise.all([this._complete(file), this.deleteMeta(file.name)]);
};
Expand Down
2 changes: 1 addition & 1 deletion test/gcs-storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('GCStorage', () => {
let storage: GCStorage;
let file: GCSFile;
const uri = 'http://api.com?upload_id=123456789';
const _fileResponse = (): { data: GCSFile } => ({ data: { ...testfile, uri } });
const _fileResponse = (): { data: GCSFile } => ({ data: { ...testfile, uri } as GCSFile });
const _createResponse = (): any => ({ headers: { location: uri } });
const req = { headers: { origin: 'http://api.com' } } as IncomingMessage;

Expand Down

0 comments on commit 7023397

Please sign in to comment.