Commit 119e0e6b authored by nanahira's avatar nanahira

reuse package

parent 664c0d45
export class PackageResult { export class PackageResult {
constructor(public checksum: Record<string, string>, public packages: Record<string, string[]>) {} constructor(public checksum: Record<string, string>, public packages: Record<string, string[]>, public fullPackage: string) {}
} }
...@@ -13,6 +13,12 @@ import readdirp from 'readdirp'; ...@@ -13,6 +13,12 @@ import readdirp from 'readdirp';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { ConsoleLogger, Injectable } from '@nestjs/common'; import { ConsoleLogger, Injectable } from '@nestjs/common';
import { createHash } from 'crypto';
export interface FileWithHash {
file: readdirp.EntryInfo;
hash: string;
}
@Injectable() @Injectable()
export class PackagerService extends ConsoleLogger { export class PackagerService extends ConsoleLogger {
...@@ -38,7 +44,7 @@ export class PackagerService extends ConsoleLogger { ...@@ -38,7 +44,7 @@ export class PackagerService extends ConsoleLogger {
this.log(`Package extracted to ${extractRoot}.`); this.log(`Package extracted to ${extractRoot}.`);
const packages: Record<string, string[]> = {}; const packagesSequence: string[][] = [];
const entries = await readdirp.promise(root, { alwaysStat: true, type: 'files_directories' }); const entries = await readdirp.promise(root, { alwaysStat: true, type: 'files_directories' });
const [directories, files] = _.partition(entries, (item) => item.stats.isDirectory()); const [directories, files] = _.partition(entries, (item) => item.stats.isDirectory());
...@@ -48,41 +54,39 @@ export class PackagerService extends ConsoleLogger { ...@@ -48,41 +54,39 @@ export class PackagerService extends ConsoleLogger {
directories.map((d) => d.path), directories.map((d) => d.path),
files.map((f) => f.path) files.map((f) => f.path)
); );
const promises: Promise<[string, string]>[] = []; const promises: Promise<string>[] = [];
const filesWithHash: FileWithHash[] = files.map((f) => ({ file: f, hash: checksum[f.path] }));
// 整包 // 整包
const archive = `${uuidv4()}.tar.gz`; packagesSequence.push(files.map((f) => f.path));
packages[archive] = files.map((f) => f.path); promises.push(this.archive(root, tarballRoot, filesWithHash, await fs.promises.readdir(root)));
promises.push(this.archive(archive, root, tarballRoot, await fs.promises.readdir(root)));
// 散包 // 散包
const buckets: Record<string, [string[], number]> = {}; const buckets: Record<string, [FileWithHash[], number]> = {};
for (const file of files) { for (const file of filesWithHash) {
if (file.stats.size < this.bucket_enter) { if (file.file.stats.size < this.bucket_enter) {
const extname = path.extname(file.basename); const extname = path.extname(file.file.basename);
buckets[extname] ??= [[], 0]; buckets[extname] ??= [[], 0];
const bucket = buckets[extname]; const bucket = buckets[extname];
if (bucket[1] + file.stats.size >= this.bucket_max) { if (bucket[1] + file.file.stats.size >= this.bucket_max) {
const archive = `${uuidv4()}.tar.gz`; packagesSequence.push(bucket[0].map((f) => f.file.path));
packages[archive] = bucket[0]; promises.push(this.archive(root, tarballRoot, bucket[0]));
promises.push(this.archive(archive, root, tarballRoot, bucket[0], checksum));
bucket[0] = []; bucket[0] = [];
bucket[1] = 0; bucket[1] = 0;
} else { } else {
bucket[0].push(file.path); bucket[0].push(file);
bucket[1] += file.stats.size; bucket[1] += file.file.stats.size;
} }
} else { } else {
const archive = `${uuidv4()}.tar.gz`; packagesSequence.push([file.file.path]);
packages[archive] = [file.path]; promises.push(this.archive(root, tarballRoot, [file]));
promises.push(this.archive(archive, root, tarballRoot, [file.path], checksum));
} }
} }
for (const bucket of Object.values(buckets)) { for (const bucket of Object.values(buckets)) {
if (bucket[0].length) { if (bucket[0].length) {
const archive = `${uuidv4()}.tar.gz`; packagesSequence.push(bucket[0].map((f) => f.file.path));
packages[archive] = bucket[0]; promises.push(this.archive(root, tarballRoot, bucket[0]));
promises.push(this.archive(archive, root, tarballRoot, bucket[0], checksum));
} }
} }
...@@ -90,16 +94,15 @@ export class PackagerService extends ConsoleLogger { ...@@ -90,16 +94,15 @@ export class PackagerService extends ConsoleLogger {
const gotPackages = await Promise.all(promises); // 这个 await 过后,checksum 和 打包上传都已经跑完了 const gotPackages = await Promise.all(promises); // 这个 await 过后,checksum 和 打包上传都已经跑完了
for (const differentPackages of gotPackages.filter((p) => p[0] !== p[1])) { const packages: Record<string, string[]> = {};
const [originalPackage, gotPackage] = differentPackages; for (let i = 0; i < packagesSequence.length; ++i) {
packages[gotPackage] = packages[originalPackage]; packages[gotPackages[i]] = packagesSequence[i];
delete packages[originalPackage];
} }
this.log({ checksum, packages }); this.log({ checksum, packages });
await fs.promises.rm(root, { recursive: true }); await fs.promises.rm(root, { recursive: true });
await fs.promises.rm(tarballRoot, { recursive: true }); await fs.promises.rm(tarballRoot, { recursive: true });
return new PackageResult(checksum, packages); return new PackageResult(checksum, packages, gotPackages[0]);
} }
async checksum(root: string, directories: string[], files: string[]): Promise<Record<string, string>> { async checksum(root: string, directories: string[], files: string[]): Promise<Record<string, string>> {
...@@ -111,20 +114,22 @@ export class PackagerService extends ConsoleLogger { ...@@ -111,20 +114,22 @@ export class PackagerService extends ConsoleLogger {
]); ]);
} }
async archive( async archive(root: string, tarballRoot: string, files: FileWithHash[], altFiles?: string[]): Promise<string> {
archive: string = `${uuidv4()}.tar.gz`, const archive =
root: string, createHash('sha512')
tarballRoot: string, .update(files.map((f) => `${f.file.path}${f.hash}`).join(''))
files: string[], .digest('hex') + '.tar.gz';
checksum: Record<string, string> = {} if (await this.s3.fileExists(archive)) {
): Promise<[string, string]> { return archive;
}
const archivePath = path.join(tarballRoot, archive); const archivePath = path.join(tarballRoot, archive);
await this.spawnAsync('tar', ['-zcvf', archivePath].concat(files), { this.log(`Packaging archive ${archivePath} with ${files.length} files.`);
await this.spawnAsync('tar', ['-zcvf', archivePath].concat(altFiles || files.map((f) => f.file.path)), {
cwd: root, cwd: root,
}); });
const fileSize = (await fs.promises.stat(archivePath)).size; const fileSize = (await fs.promises.stat(archivePath)).size;
await this.s3.uploadFile(archive, fs.createReadStream(archivePath), { ContentType: 'application/tar+gzip', ContentLength: fileSize }); await this.s3.uploadFile(archive, fs.createReadStream(archivePath), { ContentType: 'application/tar+gzip', ContentLength: fileSize });
return [archive, archive]; return archive;
} }
private spawnAsync(command: string, args: string[], options: child_process.SpawnOptions) { private spawnAsync(command: string, args: string[], options: child_process.SpawnOptions) {
......
...@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'; ...@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { ListObjectsCommand, PutObjectCommand, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3'; import { ListObjectsCommand, PutObjectCommand, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import internal from 'stream'; import internal from 'stream';
import Path from 'path';
export class S3Service extends ConsoleLogger { export class S3Service extends ConsoleLogger {
private readonly bucket: string; private readonly bucket: string;
...@@ -33,28 +34,32 @@ export class S3Service extends ConsoleLogger { ...@@ -33,28 +34,32 @@ export class S3Service extends ConsoleLogger {
async listObjects(path: string) { async listObjects(path: string) {
const command = new ListObjectsCommand({ const command = new ListObjectsCommand({
Bucket: this.bucket, Bucket: this.bucket,
Prefix: path, Prefix: this.getPathWithPrefix(path),
}); });
return this.s3.send(command); return this.s3.send(command);
} }
async fileExists(path: string) {
const objects = await this.listObjects(path);
// this.log(objects);
return objects.Contents && objects.Contents.some((obj) => obj.Key === this.getPathWithPrefix(path));
}
private getPathWithPrefix(filename: string) { private getPathWithPrefix(filename: string) {
return this.prefix ? `${this.prefix}/${filename}` : filename; return this.prefix ? `${this.prefix}/${filename}` : filename;
} }
async uploadAssets(file: Express.Multer.File) { async uploadAssets(file: Express.Multer.File) {
const fileSuffix = file.originalname.split('.').pop(); const fileSuffix = Path.extname(file.originalname);
const hash = createHash('sha512').update(file.buffer).digest('hex'); const hash = createHash('sha512').update(file.buffer).digest('hex');
const filename = `${hash}.${fileSuffix}`; const path = fileSuffix ? `${hash}.${fileSuffix}` : hash;
const path = this.getPathWithPrefix(filename); const downloadUrl = `${this.cdnUrl}/${path}`;
const downloadUrl = `${this.cdnUrl}/${filename}`;
const checkExisting = await this.listObjects(path);
try { try {
if (checkExisting.Contents && checkExisting.Contents.some((obj) => obj.Key === path)) { if (await this.fileExists(path)) {
// already uploaded // already uploaded
return downloadUrl; return downloadUrl;
} else { } else {
return this.uploadFile(filename, file.buffer, { ContentType: file.mimetype }); return this.uploadFile(path, file.buffer, { ContentType: file.mimetype });
} }
} catch (e) { } catch (e) {
this.error(`Failed to upload assets ${path}: ${e.toString()}`); this.error(`Failed to upload assets ${path}: ${e.toString()}`);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment