Skip to content

Commit 7024c10

Browse files
dsanders11cacieprinsBlackHole1
authored
fix: handle cross-device cache locations (#336)
Co-authored-by: Cacie Prins <[email protected]> Co-authored-by: Kevin Cui <[email protected]>
1 parent b171f10 commit 7024c10

File tree

2 files changed

+37
-2
lines changed

2 files changed

+37
-2
lines changed

src/Cache.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,22 @@ export class Cache {
4949
d('* Replacing existing file');
5050
await fs.promises.rm(cachePath, { recursive: true, force: true });
5151
}
52-
await fs.promises.rename(currentPath, cachePath);
52+
53+
try {
54+
await fs.promises.rename(currentPath, cachePath);
55+
} catch (err) {
56+
if ((err as NodeJS.ErrnoException).code === 'EXDEV') {
57+
// Cross-device link, fallback to copy and delete
58+
await fs.promises.cp(currentPath, cachePath, {
59+
force: true,
60+
recursive: true,
61+
verbatimSymlinks: true,
62+
});
63+
await fs.promises.rm(currentPath, { force: true, recursive: true });
64+
} else {
65+
throw err;
66+
}
67+
}
5368

5469
return cachePath;
5570
}

test/Cache.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import os from 'node:os';
33
import path from 'node:path';
44
import util from 'node:util';
55

6-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
77

88
import { Cache } from '../src/Cache';
99

@@ -88,5 +88,25 @@ describe('Cache', () => {
8888
expect(cachePath.startsWith(cacheDir)).toEqual(true);
8989
expect(await util.promisify(fs.readFile)(cachePath, 'utf8')).toEqual('example content');
9090
});
91+
92+
it('can handle cross-device cache paths', async () => {
93+
const error: NodeJS.ErrnoException = new Error('EXDEV: cross-device link not permitted');
94+
error.code = 'EXDEV';
95+
96+
const spy = vi.spyOn(fs.promises, 'rename').mockRejectedValueOnce(error);
97+
98+
try {
99+
const originalFolder = path.resolve(cacheDir, sanitizedDummyUrl);
100+
await fs.promises.mkdir(originalFolder, { recursive: true });
101+
const originalPath = path.resolve(originalFolder, 'original.txt');
102+
await util.promisify(fs.writeFile)(originalPath, 'example content');
103+
const cachePath = await cache.putFileInCache(dummyUrl, originalPath, 'test.txt');
104+
expect(cachePath.startsWith(cacheDir)).toEqual(true);
105+
expect(await util.promisify(fs.readFile)(cachePath, 'utf8')).toEqual('example content');
106+
expect(spy).toHaveBeenCalled();
107+
} finally {
108+
spy.mockRestore();
109+
}
110+
});
91111
});
92112
});

0 commit comments

Comments
 (0)