diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e055dbcf7..569be94b9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,9 +1,18 @@ import { execSync } from 'node:child_process'; -import { createWriteStream, existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; +import { + createWriteStream, + existsSync, + readdirSync, + readFileSync, + realpathSync, + statSync, + writeFileSync, +} from 'node:fs'; import { mkdir, readFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, join, relative, resolve, sep } from 'node:path'; import process from 'node:process'; +import { finished } from 'node:stream/promises'; import { DurationFormatter as SapphireDurationFormatter, TimeTypes } from '@sapphire/duration'; import { Timestamp } from '@sapphire/timestamp'; @@ -573,9 +582,15 @@ export const createActZip = async (zipName: string, pathsToZip: string[], cwd: s }); archive.pipe(writeStream); - pathsToZip.forEach((filePath) => archive.file(join(cwd, filePath), { name: filePath })); + pathsToZip.forEach((filePath) => { + const absolutePath = join(cwd, filePath); + // The platform unzips links as links; send a linked file as its content, as the multi-file upload does. + archive.file(absolutePath, { name: filePath, stats: statSync(absolutePath) }); + }); await archive.finalize(); + // The archive ends before the file stream has written all of it, and push reads the file right away. + await finished(writeStream); }; export interface LocalInput { diff --git a/test/local/lib/utils.test.ts b/test/local/lib/utils.test.ts index 719efa9d2..164d78950 100644 --- a/test/local/lib/utils.test.ts +++ b/test/local/lib/utils.test.ts @@ -1,6 +1,8 @@ -import { existsSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; +import process from 'node:process'; +import AdmZip from 'adm-zip'; import axios from 'axios'; import { execWithLog } from '../../../src/lib/exec.js'; @@ -86,6 +88,31 @@ describe('Utils', () => { expect(existsSync(join(tempFolder, file))).toBeFalsy(), ); }); + + it('has the whole zip written when it resolves', async () => { + const zipName = joinPath('complete.zip'); + await createActZip(zipName, FILES, tmpPath); + + expect( + new AdmZip(readFileSync(zipName)) + .getEntries() + .map((entry) => entry.entryName) + .sort(), + ).toEqual([...FILES].sort()); + }); + + // Creating a symlink on Windows needs extra rights. + it.skipIf(process.platform === 'win32')('zips a file reached through a symlink as its content', async () => { + // A link entry would hold the link's target path, which the platform unzips as a link to a file the upload + // may not hold. + const zipName = joinPath('linked.zip'); + writeFileSync(joinPath('linked-target.json'), '{"title":"Input"}'); + symlinkSync('linked-target.json', joinPath('linked.json')); + + await createActZip(zipName, ['linked.json'], tmpPath); + + expect(new AdmZip(readFileSync(zipName)).getEntry('linked.json')?.getData().toString()).toBe('{"title":"Input"}'); + }); }); describe('downloadAndUnzip()', () => {