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 b36495a
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 67 deletions.
66 changes: 18 additions & 48 deletions examples/express.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,27 @@
import * as express from 'express';
import { DiskFile, DiskStorage, OnComplete, uploadx, UploadxResponse } from 'node-uploadx';
import { DiskFile, 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();
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('sha1');
await file.move(join('upload', file.originalName));
await file.lock(() => res.json({ ...file, sha1 }));
await file.delete();
return res.json({ ...file, sha1 });
};

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 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' }]
}
}
});

app.use('/files', uploadx({ storage }));
app.all(
'/files',
uploadx.upload({
directory: 'upload',
expiration: { maxAge: '12h', purgeInterval: '1h' }
}),
onComplete
);

app.listen(3002, () => {
console.log('listening on port:', 3002);
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
4 changes: 2 additions & 2 deletions test/disk-storage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { posix } from 'path';
import { DiskStorage, fsp } from '../packages/core/src';
import { DiskStorage, DiskStorageOptions, fsp } from '../packages/core/src';
import { storageOptions } from './fixtures';
import { FileWriteStream, RequestReadStream } from './fixtures/streams';
import { filename, metafile, testfile } from './fixtures/testfile';
Expand All @@ -26,7 +26,7 @@ jest.mock('../packages/core/src/utils/fs', () => {
});

describe('DiskStorage', () => {
const options = { ...storageOptions, directory };
const options = { ...storageOptions, directory } as DiskStorageOptions;
let storage: DiskStorage;
let mockReadable: RequestReadStream;
const createFile = (): Promise<any> => {
Expand Down
4 changes: 2 additions & 2 deletions test/multipart.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { join } from 'path';
import * as request from 'supertest';
import { multipart } from '../packages/core/src';
import { multipart, DiskStorageOptions } from '../packages/core/src';
import { root, storageOptions } from './fixtures';
import { app } from './fixtures/app';
import { metadata, srcpath } from './fixtures/testfile';
Expand All @@ -12,7 +12,7 @@ describe('::Multipart', () => {
const files: string[] = [];
const basePath = '/multipart';
const directory = join(root, 'multipart');
const opts = { ...storageOptions, directory };
const opts = { ...storageOptions, directory } as DiskStorageOptions;
app.use(basePath, multipart(opts));

beforeAll(() => cleanup(directory));
Expand Down
2 changes: 1 addition & 1 deletion test/s3-storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('S3Storage', () => {
...testfile,
UploadId: '123456789',
Parts: []
};
} as unknown as S3File;
});

describe('.create()', () => {
Expand Down
10 changes: 8 additions & 2 deletions test/tus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
import * as fs from 'fs';
import { join } from 'path';
import * as request from 'supertest';
import { BaseStorage, serializeMetadata, tus, TUS_RESUMABLE } from '@uploadx/core';
import {
BaseStorage,
DiskStorageOptions,
serializeMetadata,
tus,
TUS_RESUMABLE
} from '@uploadx/core';
import { root, storageOptions } from './fixtures';
import { app } from './fixtures/app';
import { metadata, srcpath } from './fixtures/testfile';
Expand All @@ -12,7 +18,7 @@ describe('::Tus', () => {
let uri: string;
const basePath = '/tus';
const directory = join(root, 'tus');
const opts = { ...storageOptions, directory };
const opts = { ...storageOptions, directory } as DiskStorageOptions;
app.use(basePath, tus(opts));

beforeAll(() => cleanup(directory));
Expand Down
4 changes: 2 additions & 2 deletions test/uploadx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as fs from 'fs';
import { join } from 'path';
import * as request from 'supertest';
import { uploadx } from '../packages/core/src';
import { uploadx, DiskStorageOptions } from '../packages/core/src';
import { root, storageOptions, userPrefix } from './fixtures';
import { app } from './fixtures/app';
import { metadata, srcpath } from './fixtures/testfile';
Expand All @@ -13,7 +13,7 @@ describe('::Uploadx', () => {
let start: number;
const basePath = '/uploadx';
const directory = join(root, 'uploadx');
const opts = { ...storageOptions, directory, maxMetadataSize: 250 };
const opts = { ...storageOptions, directory, maxMetadataSize: 250 } as DiskStorageOptions;
app.use(basePath, uploadx(opts));

beforeAll(() => cleanup(directory));
Expand Down

0 comments on commit b36495a

Please sign in to comment.