Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import pathModule from 'path';
import { createBufferFromPath } from './buffer.js';
import { getCodeFromUnknownError, UserError } from './error.js';

const concurrentModificationHint =
export const concurrentModificationHint =
'Make sure that no other process (e.g. a build or file sync client) modifies the folder while the command is running.';
const readPermissionHint = 'Make sure that you have permission to read the file.';
const writePermissionHint = 'Make sure that you have permission to write to the folder or choose a different path.';
Expand Down Expand Up @@ -65,12 +65,15 @@ export const readFileFromDirectory = async (path: string): Promise<Buffer> => {
const code = getCodeFromUnknownError(error);
const hint = code ? unreadableFileErrorHints[code] : undefined;
if (hint) {
throw new UserError(`The file could not be read: ${path}. ${hint}`);
throw createUnreadableFileError(path, hint);
}
throw error;
}
};

export const createUnreadableFileError = (path: string, hint: string): UserError =>
new UserError(`The file could not be read: ${path}. ${hint}`);

export const directoryContainsSymlinks = async (path: string): Promise<boolean> => {
const dirEntries = await fs.promises.readdir(path, { withFileTypes: true, recursive: true }).catch(() => []);
return dirEntries.some((dirEntry) => dirEntry.isSymbolicLink());
Expand Down
37 changes: 37 additions & 0 deletions src/utils/zip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from 'vitest';
import { UserError } from './error.js';
import zip from './zip.js';

const { mockAddLocalFolder } = vi.hoisted(() => ({ mockAddLocalFolder: vi.fn() }));

vi.mock('adm-zip', () => ({
default: function () {
return { addLocalFolder: mockAddLocalFolder, getEntries: () => [], toBuffer: () => Buffer.alloc(0) };
},
}));

describe('zipFolder', () => {
it('should throw a user error if a file vanished while zipping', async () => {
mockAddLocalFolder.mockImplementationOnce(() => {
throw new Error('ADM-ZIP: File not found: "www/index.html"');
});

const promise = zip.zipFolder('www');

await expect(promise).rejects.toThrow(UserError);
await expect(promise).rejects.toThrow(
'The file could not be read: www/index.html. Make sure that no other process',
);
});

it('should rethrow other errors', async () => {
mockAddLocalFolder.mockImplementationOnce(() => {
throw new Error('ADM-ZIP: Invalid filename');
});

const promise = zip.zipFolder('www');

await expect(promise).rejects.not.toThrow(UserError);
await expect(promise).rejects.toThrow('ADM-ZIP: Invalid filename');
});
});
20 changes: 19 additions & 1 deletion src/utils/zip.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UserError } from '@/utils/error.js';
import { concurrentModificationHint, createUnreadableFileError } from '@/utils/file.js';
import AdmZip from 'adm-zip';
import { globby } from 'globby';
import path from 'path';
Expand All @@ -20,7 +21,15 @@ class ZipImpl implements Zip {

async zipFolder(sourceFolder: string): Promise<Buffer> {
const zip = new AdmZip();
zip.addLocalFolder(sourceFolder);
try {
zip.addLocalFolder(sourceFolder);
} catch (error) {
const vanishedFilePath = getVanishedFilePath(error);
if (vanishedFilePath) {
throw createUnreadableFileError(vanishedFilePath, concurrentModificationHint);
}
throw error;
}
// Strip external file attributes from every entry. `addLocalFolder` copies
// the source's Unix mode bits onto each entry, so a directory with e.g.
// `0500` on a CI checkout ends up in the bundle and makes `zip4j` on
Expand Down Expand Up @@ -75,6 +84,15 @@ class ZipImpl implements Zip {
}
}

// adm-zip lists all files first and reads them afterwards. A file that was
// deleted in between is reported with this message and without an error code.
const getVanishedFilePath = (error: unknown): string | undefined => {
if (!(error instanceof Error)) {
return undefined;
}
return error.message.match(/^ADM-ZIP: File not found: "(.+)"$/)?.[1];
};

const zip: Zip = new ZipImpl();

export default zip;
Loading