Skip to content

Commit

Permalink
feat: extended completed file
Browse files Browse the repository at this point in the history
  • Loading branch information
kukhariev committed Aug 31, 2021
1 parent 6d066b8 commit 56dcdf2
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 56 deletions.
56 changes: 12 additions & 44 deletions examples/express.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,25 @@
import * as express from 'express';
import { DiskFile, DiskStorage, OnComplete, uploadx, UploadxResponse } from 'node-uploadx';
import { DiskFile, DiskStorage, uploadx } from 'node-uploadx';
import { join } from 'path';

const app = express();

const auth: express.Handler = (req, res, next) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(req as any)['user'] = { id: '92be348f-172d-5f69-840d-100f79e4d1ef' };
next();
};

app.use(auth);

type OnCompleteBody = {
message: string;
id: string;
};

const onComplete: OnComplete<DiskFile, UploadxResponse<OnCompleteBody>> = file => {
const message = `File upload is finished, path: ${file.name}`;
console.log(message);
return {
statusCode: 200,
message,
id: file.id,
headers: { ETag: file.id }
};
const onComplete: express.RequestHandler = async (req, res, next) => {
const file = req.body as DiskFile;
await file.lock(() => res.status(423).json({ message: 'processing' }));
const sha1 = await file.hash();
await file.move(join('upload', file.originalName));
await file.delete();
await file.lock(() => res.json({ ...file, sha1 }));
return res.json({ ...file, sha1 });
};

const storage = new DiskStorage({
directory: 'upload',
maxMetadataSize: '1mb',
onComplete,
expiration: { maxAge: '1h', purgeInterval: '10min' },
validation: {
mime: { value: ['video/*'], response: [415, { message: 'video only' }] },
size: {
value: 500_000_000,
isValid(file) {
this.response = [
412,
{ message: `The file size(${file.size}) is larger than ${this.value as number} bytes` }
];
return file.size <= this.value;
}
},
mtime: {
isValid: file => !!file.metadata.lastModified,
response: [403, { message: 'Missing `lastModified` property' }]
}
}
expiration: { maxAge: '12h', purgeInterval: '1h' }
});

app.use('/files', uploadx({ storage }));
app.use('/files', uploadx.upload({ storage }), onComplete);

app.listen(3002, () => {
console.log('listening on port:', 3002);
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/handlers/base-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as http from 'http';
import * as url from 'url';
import {
BaseStorage,
Completed,
DiskStorage,
DiskStorageOptions,
File,
Expand Down Expand Up @@ -129,14 +130,16 @@ export abstract class BaseHandler<TFile extends Readonly<File>>

handler
.call(this, req, res)
.then(async (file: TFile | UploadList): Promise<void> => {
.then(async (file: TFile | UploadList | Completed<TFile>): Promise<void> => {
if ('status' in file && file.status) {
this.log('[%s]: %s', file.status, file.name);
this.listenerCount(file.status) && this.emit(file.status, file);
if (file.status === 'completed') {
req['_body'] = true;
req['body'] = file;
const completed = (await this.storage.onComplete(file)) as UploadxResponse;
const completed = (await this.storage.onComplete(
file as Completed<TFile>
)) as UploadxResponse;
next ? next() : this.finish(req as any, res, completed || file);
}
return;
Expand Down
34 changes: 31 additions & 3 deletions packages/core/src/storages/disk-storage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import * as http from 'http';
import { resolve as pathResolve } from 'path';
import { ensureFile, ERRORS, fail, fsp, getWriteStream, HttpError } from '../utils';
import {
ensureFile,
ERRORS,
fail,
fileChecksum,
fsp,
getWriteStream,
HttpError,
move
} from '../utils';
import { File, FileInit, FilePart, hasContent, isCompleted, isValidPart } from './file';
import { BaseStorage, BaseStorageOptions } from './storage';
import { METAFILE_EXTNAME, MetaStorage } from './meta-storage';
import { LocalMetaStorage, LocalMetaStorageOptions } from './local-meta-storage';

const INVALID_OFFSET = -1;

export class DiskFile extends File {}
export interface DiskFile extends File {
lock: (lockFn: () => any) => Promise<any>;
move: (dest: string) => Promise<any>;
copy: (dest: string) => Promise<any>;
delete: () => Promise<any>;
hash: (algorithm?: 'sha1' | 'md5', encoding?: 'hex' | 'base64') => Promise<string>;
}

export type DiskStorageOptions = BaseStorageOptions<DiskFile> & {
/**
Expand Down Expand Up @@ -51,8 +66,20 @@ export class DiskStorage extends BaseStorage<DiskFile> {
return super.normalizeError(error);
}

buildCompletedFile(file: DiskFile): DiskFile {
const completed = { ...file };
completed.lock = async lockFn => Promise.resolve('TODO:');
completed.delete = () => this.delete(file.name);
completed.hash = (algorithm?: 'sha1' | 'md5', encoding?: 'hex' | 'base64') =>
fileChecksum(this.getFilePath(file.name), algorithm, encoding);
completed.copy = async (dest: string) => fsp.copyFile(this.getFilePath(file.name), dest);
completed.move = async (dest: string) => move(this.getFilePath(file.name), dest);

return completed;
}

async create(req: http.IncomingMessage, fileInit: FileInit): Promise<DiskFile> {
const file = new DiskFile(fileInit);
const file = new File(fileInit) as DiskFile;
file.name = this.namingFunction(file);
await this.validate(file);
const path = this.getFilePath(file.name);
Expand All @@ -72,6 +99,7 @@ export class DiskStorage extends BaseStorage<DiskFile> {
if (file.bytesWritten === INVALID_OFFSET) return fail(ERRORS.FILE_CONFLICT);
if (isCompleted(file)) {
await this.saveMeta(file);
return this.buildCompletedFile(file);
}
return file;
} catch (err) {
Expand Down
38 changes: 35 additions & 3 deletions packages/core/src/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
import { createWriteStream, promises as fsp, WriteStream } from 'fs';
import { createHash, HexBase64Latin1Encoding } from 'crypto';
import { createReadStream, createWriteStream, promises as fsp, WriteStream } from 'fs';
import { dirname, posix } from 'path';

function isError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error;
}

export function fileChecksum(
filePath: string,
algorithm: 'sha1' | 'md5' = 'md5',
encoding: HexBase64Latin1Encoding = 'hex'
): Promise<string> {
return new Promise(resolve => {
const hash = createHash(algorithm);
createReadStream(filePath)
.on('data', data => hash.update(data))
.on('end', () => resolve(hash.digest(encoding)));
});
}

export async function copy(src: string, dest: string): Promise<void> {
return fsp.copyFile(src, dest);
}

export async function move(src: string, dest: string): Promise<void> {
try {
await fsp.rename(src, dest);
} catch (e) {
if (isError(e) && e.code === 'EXDEV') {
await copy(src, dest);
await fsp.unlink(src);
}
}
}
/**
* Ensures that the directory exists
* @param dir
*/
export async function ensureDir(dir: string): Promise<void> {
await fsp.mkdir(dir, { recursive: true });
export function ensureDir(dir: string): Promise<void> {
return fsp.mkdir(dir, { recursive: true });
}

/**
Expand Down
12 changes: 8 additions & 4 deletions packages/s3/src/s3-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ import { S3MetaStorage, S3MetaStorageOptions } from './s3-meta-storage';

const BUCKET_NAME = 'node-uploadx';

export class S3File extends File {
export interface S3File extends File {
Parts?: Part[];
UploadId = '';
UploadId?: string;
uri?: string;
lock: (lockFn: () => any) => Promise<any>;
move: (dest: any) => Promise<any>;
copy: (dest: any) => Promise<any>;
delete: () => Promise<any>;
}

export type S3StorageOptions = BaseStorageOptions<S3File> &
Expand Down Expand Up @@ -121,7 +125,7 @@ export class S3Storage extends BaseStorage<S3File> {
}

async create(req: http.IncomingMessage, config: FileInit): Promise<S3File> {
const file = new S3File(config);
const file = new File(config) as S3File;
file.name = this.namingFunction(file);
await this.validate(file);
try {
Expand Down Expand Up @@ -226,7 +230,7 @@ export class S3Storage extends BaseStorage<S3File> {
}

private _checkBucket(): void {
this.client.send(new HeadBucketCommand({ Bucket: this.bucket }), (err: AWSError, data) => {
this.client.send(new HeadBucketCommand({ Bucket: this.bucket }), (err: AWSError) => {
if (err) {
throw err;
}
Expand Down

0 comments on commit 56dcdf2

Please sign in to comment.