Skip to content

Commit a6172c8

Browse files
committed
fix: remove package version files
1 parent 9f4b8eb commit a6172c8

File tree

6 files changed

+287
-0
lines changed

6 files changed

+287
-0
lines changed

app/core/service/PackageManagerService.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
PackageManifestType,
3131
PackageRepository,
3232
} from '../../repository/PackageRepository.js';
33+
import type { PackageVersionFileRepository } from '../../repository/PackageVersionFileRepository.js';
3334
import type { PackageVersionBlockRepository } from '../../repository/PackageVersionBlockRepository.js';
3435
import type { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository.js';
3536
import type { DistRepository } from '../../repository/DistRepository.js';
@@ -100,6 +101,8 @@ export class PackageManagerService extends AbstractService {
100101
@Inject()
101102
private readonly packageRepository: PackageRepository;
102103
@Inject()
104+
private readonly packageVersionFileRepository: PackageVersionFileRepository;
105+
@Inject()
103106
private readonly packageVersionBlockRepository: PackageVersionBlockRepository;
104107
@Inject()
105108
private readonly packageVersionDownloadRepository: PackageVersionDownloadRepository;
@@ -720,6 +723,28 @@ export class PackageManagerService extends AbstractService {
720723
]);
721724
// remove from repository
722725
await this.packageRepository.removePackageVersion(pkgVersion);
726+
727+
// remove package version files
728+
await this.removePackageVersionFileAndDist(pkgVersion);
729+
}
730+
731+
public async removePackageVersionFileAndDist(pkgVersion: PackageVersion) {
732+
const fileDists =
733+
await this.packageVersionFileRepository.findPackageVersionFiles(
734+
pkgVersion.packageVersionId
735+
);
736+
737+
// remove nfs dists
738+
await Promise.all(
739+
fileDists.map(dist => this.distRepository.destroyDist(dist))
740+
);
741+
742+
// remove package version files from repository
743+
await this.packageVersionFileRepository.removePackageVersionFiles(
744+
pkgVersion.packageVersionId
745+
);
746+
747+
return fileDists;
723748
}
724749

725750
public async unpublishPackage(pkg: Package) {

app/core/service/PackageVersionFileService.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,32 @@ export class PackageVersionFileService extends AbstractService {
335335
}
336336
}
337337

338+
async removePackageVersionFiles(pkgVersion: PackageVersion) {
339+
const files: PackageVersionFile[] = [];
340+
if (!this.config.cnpmcore.enableUnpkg) return files;
341+
if (!this.config.cnpmcore.enableSyncUnpkgFiles) return files;
342+
const pkg = await this.packageRepository.findPackageByPackageId(
343+
pkgVersion.packageId
344+
);
345+
if (!pkg) return files;
346+
347+
const fileDists =
348+
await this.packageManagerService.removePackageVersionFileAndDist(
349+
pkgVersion
350+
);
351+
return fileDists.map(dist => {
352+
const { directory, name } = this.#getDirectoryAndName(dist.path);
353+
return PackageVersionFile.create({
354+
packageVersionId: pkgVersion.packageVersionId,
355+
directory,
356+
name,
357+
dist,
358+
contentType: mimeLookup(dist.path),
359+
mtime: pkgVersion.publishTime,
360+
});
361+
});
362+
}
363+
338364
async #savePackageVersionFile(
339365
pkg: Package,
340366
pkgVersion: PackageVersion,

app/port/controller/PackageVersionFileController.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ export class PackageVersionFileController extends AbstractController {
9999
return files.map(file => formatFileItem(file));
100100
}
101101

102+
@HTTPMethod({
103+
// DELETE /:fullname/:versionSpec/files
104+
path: `/:fullname(${FULLNAME_REG_STRING})/:versionSpec/files`,
105+
method: HTTPMethodEnum.DELETE,
106+
})
107+
@Middleware(AdminAccess)
108+
async removeFiles(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() versionSpec: string) {
109+
ctx.tValidate(Spec, `${fullname}@${versionSpec}`);
110+
this.#requireUnpkgEnable();
111+
const [ scope, name ] = getScopeAndName(fullname);
112+
const { packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
113+
scope, name, versionSpec);
114+
if (!packageVersion) {
115+
throw new NotFoundError(`${fullname}@${versionSpec} not found`);
116+
}
117+
const dists = await this.packageVersionFileService.removePackageVersionFiles(packageVersion);
118+
return dists.map(file => formatFileItem(file));
119+
}
120+
102121
@HTTPMethod({
103122
// GET /:fullname/:versionSpec/files => /:fullname/:versionSpec/files/${pkg.main}
104123
// GET /:fullname/:versionSpec/files?meta

app/repository/PackageVersionFileRepository.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ export class PackageVersionFileRepository extends AbstractRepository {
5050
);
5151
}
5252

53+
async findPackageVersionFiles(packageVersionId: string) {
54+
const fileModels = await this.PackageVersionFile.find({
55+
packageVersionId,
56+
});
57+
const distIds = fileModels.map(item => item.distId);
58+
const distModels = await this.Dist.find({ distId: distIds });
59+
60+
return distModels.map(distModel =>
61+
ModelConvertor.convertModelToEntity(distModel, DistEntity)
62+
);
63+
}
64+
5365
async listPackageVersionFiles(packageVersionId: string, directory: string) {
5466
const isRoot = directory === '/';
5567
const where = isRoot
@@ -96,6 +108,39 @@ export class PackageVersionFileRepository extends AbstractRepository {
96108
return { files, directories: Array.from(subDirectories) };
97109
}
98110

111+
async removePackageVersionFiles(packageVersionId: string) {
112+
const fileDists = await this.findPackageVersionFiles(packageVersionId);
113+
const distIds = fileDists.map(dist => dist.distId);
114+
115+
await this.PackageVersionFile.transaction(async transaction => {
116+
const removeCount = await this.PackageVersionFile.remove(
117+
{ packageVersionId },
118+
true,
119+
transaction
120+
);
121+
this.logger.info(
122+
'[PackageVersionFileRepository:removePackageVersionFiles:remove] %d rows in PackageVersionFile, packageVersionId: %s',
123+
removeCount,
124+
packageVersionId
125+
);
126+
127+
const distCount = await this.Dist.remove(
128+
{
129+
distId: distIds,
130+
},
131+
true,
132+
transaction
133+
);
134+
this.logger.info(
135+
'[PackageVersionFileRepository:removePackageVersionFiles:remove] %d rows in Dist, packageVersionId: %s',
136+
distCount,
137+
packageVersionId
138+
);
139+
});
140+
141+
return fileDists;
142+
}
143+
99144
async hasPackageVersionFiles(packageVersionId: string) {
100145
const model = await this.PackageVersionFile.findOne({ packageVersionId });
101146
return !!model;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import assert from 'node:assert/strict';
2+
import { app, mock } from '@eggjs/mock/bootstrap';
3+
4+
import { TestUtil, type TestUser } from '../../../../test/TestUtil.js';
5+
6+
describe('test/port/controller/PackageVersionFileController/removeFiles.test.ts', () => {
7+
let publisher: TestUser;
8+
let adminUser: TestUser;
9+
beforeEach(async () => {
10+
publisher = await TestUtil.createUser();
11+
adminUser = await TestUtil.createAdmin();
12+
});
13+
14+
describe('[DELETE /:fullname/:versionSpec/files] removeFiles()', () => {
15+
it('should 404 when enableUnpkg = false', async () => {
16+
mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true);
17+
mock(app.config.cnpmcore, 'enableUnpkg', false);
18+
const pkg = await TestUtil.getFullPackage({
19+
name: 'foo',
20+
version: '1.0.0',
21+
versionObject: {
22+
description: 'work with utf8mb4 💩, 𝌆 utf8_unicode_ci, foo𝌆bar 🍻',
23+
},
24+
});
25+
await app
26+
.httpRequest()
27+
.put(`/${pkg.name}`)
28+
.set('authorization', publisher.authorization)
29+
.set('user-agent', publisher.ua)
30+
.send(pkg)
31+
.expect(201);
32+
const res = await app
33+
.httpRequest()
34+
.delete('/foo/1.0.0/files')
35+
.set('authorization', adminUser.authorization)
36+
.expect(404)
37+
.expect('content-type', 'application/json; charset=utf-8');
38+
assert.equal(res.body.error, '[NOT_FOUND] Not Found');
39+
});
40+
41+
it('should work', async () => {
42+
mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true);
43+
mock(app.config.cnpmcore, 'enableUnpkg', true);
44+
mock(app.config.cnpmcore, 'enableSyncUnpkgFiles', true);
45+
app.mockLog();
46+
47+
const pkg = await TestUtil.getFullPackage({
48+
name: 'foo',
49+
version: '1.0.0',
50+
versionObject: {
51+
description: 'work with utf8mb4 💩, 𝌆 utf8_unicode_ci, foo𝌆bar 🍻',
52+
},
53+
});
54+
55+
// publish package
56+
await app
57+
.httpRequest()
58+
.put(`/${pkg.name}`)
59+
.set('authorization', publisher.authorization)
60+
.set('user-agent', publisher.ua)
61+
.send(pkg)
62+
.expect(201);
63+
const pkgResponse = await app
64+
.httpRequest()
65+
.get(`/${pkg.name}/1.0.0`)
66+
.expect(200);
67+
const publishTime = new Date(pkgResponse.body.publish_time).toISOString();
68+
69+
// sync package version files
70+
await app
71+
.httpRequest()
72+
.put(`/${pkg.name}/1.0.0/files`)
73+
.set('authorization', adminUser.authorization)
74+
.expect(200)
75+
.expect('content-type', 'application/json; charset=utf-8');
76+
await app
77+
.httpRequest()
78+
.get(`/${pkg.name}/1.0.0/files/package.json`)
79+
.expect(200);
80+
81+
// remove package version files
82+
const packageVersionFilesDeleteResponse = await app
83+
.httpRequest()
84+
.delete(`/${pkg.name}/1.0.0/files`)
85+
.set('authorization', adminUser.authorization)
86+
.expect(200)
87+
.expect('content-type', 'application/json; charset=utf-8');
88+
assert.deepEqual(packageVersionFilesDeleteResponse.body, [
89+
{
90+
path: `/packages/${pkg.name}/1.0.0/files/package.json`,
91+
type: 'file',
92+
contentType: 'application/json',
93+
integrity:
94+
'sha512-yTg/L7tUtFK54aNH3iwgIp7sF3PiAcUrIEUo06bSNq3haIKRnagy6qOwxiEmtfAtNarbjmEpl31ZymySsECi3Q==',
95+
lastModified: publishTime,
96+
size: 209,
97+
},
98+
]);
99+
app.expectLog(
100+
`DELETE /${pkg.name}/1.0.0/files] [nfsAdapter:remove] key: /packages/foo/1.0.0/files/package.json`
101+
);
102+
app.expectLog(
103+
`DELETE /${pkg.name}/1.0.0/files] [PackageVersionFileRepository:removePackageVersionFiles:remove] 1 rows in PackageVersionFile`
104+
);
105+
app.expectLog(
106+
`DELETE /${pkg.name}/1.0.0/files] [PackageVersionFileRepository:removePackageVersionFiles:remove] 1 rows in Dist`
107+
);
108+
});
109+
110+
it('should 404 when package not exists', async () => {
111+
const res = await app
112+
.httpRequest()
113+
.delete('/@cnpm/foonot-exists/1.0.40000404/files')
114+
.set('authorization', adminUser.authorization)
115+
.expect(404);
116+
assert.equal(
117+
res.body.error,
118+
'[NOT_FOUND] @cnpm/foonot-exists@1.0.40000404 not found'
119+
);
120+
});
121+
122+
it('should 403 when non-admin request', async () => {
123+
const res = await app
124+
.httpRequest()
125+
.delete('/@cnpm/foonot-exists/1.0.40000404/files')
126+
.expect(403);
127+
assert.equal(res.body.error, '[FORBIDDEN] Not allow to access');
128+
});
129+
});
130+
});

test/port/controller/package/RemovePackageVersionController.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,48 @@ describe('test/port/controller/package/RemovePackageVersionController.test.ts',
208208
assert(res.body['dist-tags'].latest === '2.0.0');
209209
});
210210

211+
it('should remove package version files', async () => {
212+
app.mockLog();
213+
mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true);
214+
mock(app.config.cnpmcore, 'enableUnpkg', true);
215+
mock(app.config.cnpmcore, 'enableSyncUnpkgFiles', true);
216+
const { pkg } = await TestUtil.createPackage({
217+
name: 'foo',
218+
version: '1.0.0',
219+
isPrivate: false,
220+
});
221+
const res = await app.httpRequest()
222+
.get(`/${pkg.name}`)
223+
.expect(200);
224+
const adminUser = await TestUtil.createUser({ name: 'cnpmcore_admin' });
225+
const pkgVersion = res.body;
226+
227+
await app.httpRequest()
228+
.get(`/${pkg.name}/1.0.0/files/package.json`)
229+
.expect(200);
230+
231+
await app.httpRequest()
232+
.delete(`/${pkg.name}/-rev/${pkgVersion._rev}`)
233+
.set('authorization', adminUser.authorization)
234+
.set('npm-command', 'unpublish')
235+
.set('user-agent', adminUser.ua);
236+
237+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [PackageController:removeVersion] ${pkg.name}@1.0.0`);
238+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [nfsAdapter:remove] key: /packages/${pkg.name}/1.0.0/abbreviated.json`);
239+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [nfsAdapter:remove] key: /packages/${pkg.name}/1.0.0/package.json`);
240+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [nfsAdapter:remove] key: /packages/${pkg.name}/1.0.0/readme.md`);
241+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [nfsAdapter:remove] key: /packages/${pkg.name}/1.0.0/${pkg.name}-1.0.0.tgz`);
242+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [PackageRepository:removePackageVersion:remove] 4 dist rows, 1 rows`);
243+
244+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [nfsAdapter:remove] key: /packages/${pkg.name}/1.0.0/files/package.json`);
245+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [PackageVersionFileRepository:removePackageVersionFiles:remove] 1 rows in PackageVersionFile`);
246+
app.expectLog(`DELETE /${pkg.name}/-rev/${pkgVersion._rev}] [PackageVersionFileRepository:removePackageVersionFiles:remove] 1 rows in Dist`);
247+
248+
await app.httpRequest()
249+
.get(`/${pkg.name}/1.0.0/files/package.json`)
250+
.expect(404);
251+
});
252+
211253
it('should 404 when version not exists', async () => {
212254
const pkg = await TestUtil.getFullPackage({
213255
name: '@cnpm/foo',

0 commit comments

Comments
 (0)