@verdaccio/store 6.0.0-6-next.21 → 6.0.0-6-next.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/build/index.d.ts +5 -4
  3. package/build/index.js +31 -15
  4. package/build/index.js.map +1 -1
  5. package/build/lib/TransFormResults.d.ts +15 -0
  6. package/build/lib/TransFormResults.js +61 -0
  7. package/build/lib/TransFormResults.js.map +1 -0
  8. package/build/lib/search-utils.d.ts +4 -0
  9. package/build/lib/search-utils.js +59 -0
  10. package/build/lib/search-utils.js.map +1 -0
  11. package/build/lib/star-utils.d.ts +14 -0
  12. package/build/{star-utils.js → lib/star-utils.js} +16 -2
  13. package/build/lib/star-utils.js.map +1 -0
  14. package/build/lib/storage-utils.d.ts +82 -0
  15. package/build/{storage-utils.js → lib/storage-utils.js} +173 -60
  16. package/build/lib/storage-utils.js.map +1 -0
  17. package/build/lib/uplink-util.d.ts +10 -0
  18. package/build/lib/uplink-util.js +49 -0
  19. package/build/lib/uplink-util.js.map +1 -0
  20. package/build/lib/versions-utils.d.ts +28 -0
  21. package/build/lib/versions-utils.js +104 -0
  22. package/build/lib/versions-utils.js.map +1 -0
  23. package/build/local-storage.d.ts +3 -165
  24. package/build/local-storage.js +6 -1166
  25. package/build/local-storage.js.map +1 -1
  26. package/build/storage.d.ts +242 -86
  27. package/build/storage.js +1681 -548
  28. package/build/storage.js.map +1 -1
  29. package/build/type.d.ts +42 -19
  30. package/build/type.js.map +1 -1
  31. package/jest.config.js +8 -2
  32. package/package.json +71 -71
  33. package/src/index.ts +5 -4
  34. package/src/lib/TransFormResults.ts +42 -0
  35. package/src/lib/search-utils.ts +50 -0
  36. package/src/{star-utils.ts → lib/star-utils.ts} +16 -5
  37. package/src/lib/storage-utils.ts +362 -0
  38. package/src/lib/uplink-util.ts +41 -0
  39. package/src/lib/versions-utils.ts +87 -0
  40. package/src/local-storage.ts +7 -1217
  41. package/src/storage.ts +1683 -635
  42. package/src/type.ts +47 -21
  43. package/test/fixtures/config/getTarballNext-getupstream.yaml +17 -0
  44. package/test/fixtures/config/syncDoubleUplinksMetadata.yaml +25 -0
  45. package/test/fixtures/config/syncNoUplinksMetadata.yaml +17 -0
  46. package/test/fixtures/config/syncSingleUplinksMetadata.yaml +21 -0
  47. package/test/fixtures/config/updateManifest-1.yaml +16 -0
  48. package/test/fixtures/manifests/foo-npmjs.json +253 -0
  49. package/test/fixtures/manifests/foo-verdaccio.json +253 -0
  50. package/test/fixtures/tarball.tgz +0 -0
  51. package/test/helpers.ts +22 -0
  52. package/test/local-store.__disabled__.ts +553 -0
  53. package/test/search.spec.ts +4 -5
  54. package/test/star.spec.ts +31 -0
  55. package/test/storage-utils.spec.ts +129 -42
  56. package/test/storage.spec.ts +943 -33
  57. package/test/versions.spec.ts +109 -0
  58. package/tsconfig.json +6 -3
  59. package/build/search.d.ts +0 -35
  60. package/build/search.js +0 -198
  61. package/build/search.js.map +0 -1
  62. package/build/star-utils.d.ts +0 -9
  63. package/build/star-utils.js.map +0 -1
  64. package/build/storage-utils.d.ts +0 -39
  65. package/build/storage-utils.js.map +0 -1
  66. package/build/uplink-util.d.ts +0 -7
  67. package/build/uplink-util.js +0 -38
  68. package/build/uplink-util.js.map +0 -1
  69. package/src/search.ts +0 -175
  70. package/src/storage-utils.ts +0 -263
  71. package/src/uplink-util.ts +0 -32
  72. package/test/local-storage.spec.ts +0 -583
package/src/storage.ts CHANGED
@@ -1,235 +1,165 @@
1
1
  import assert from 'assert';
2
- import async, { AsyncResultArrayCallback } from 'async';
3
2
  import buildDebug from 'debug';
4
- import _ from 'lodash';
3
+ import _, { isEmpty, isNil } from 'lodash';
4
+ import { PassThrough, Readable, Transform, Writable, pipeline as streamPipeline } from 'stream';
5
+ import { pipeline } from 'stream/promises';
6
+ import { default as URL } from 'url';
5
7
 
6
8
  import { hasProxyTo } from '@verdaccio/config';
7
9
  import {
8
10
  API_ERROR,
9
11
  DIST_TAGS,
12
+ HEADER_TYPE,
10
13
  HTTP_STATUS,
14
+ SUPPORT_ERRORS,
15
+ USERS,
11
16
  errorUtils,
12
17
  pkgUtils,
18
+ searchUtils,
13
19
  validatioUtils,
14
20
  } from '@verdaccio/core';
15
21
  import { logger } from '@verdaccio/logger';
16
- import { ProxyStorage } from '@verdaccio/proxy';
17
- import { IProxy, ProxyList } from '@verdaccio/proxy';
18
- import { ReadTarball } from '@verdaccio/streams';
22
+ import { IProxy, ISyncUplinksOptions, ProxySearchParams, ProxyStorage } from '@verdaccio/proxy';
19
23
  import {
20
24
  convertDistRemoteToLocalTarballUrls,
21
25
  convertDistVersionToLocalTarballsUrl,
22
26
  } from '@verdaccio/tarball';
23
27
  import {
24
- Callback,
25
- CallbackAction,
28
+ AbbreviatedManifest,
29
+ AbbreviatedVersions,
30
+ Author,
26
31
  Config,
27
32
  DistFile,
28
33
  GenericBody,
29
- IReadTarball,
30
- IUploadTarball,
34
+ IPackageStorage,
31
35
  Logger,
36
+ Manifest,
32
37
  MergeTags,
33
- Package,
34
38
  StringValue,
35
39
  Token,
36
40
  TokenFilter,
37
41
  Version,
38
- Versions,
39
42
  } from '@verdaccio/types';
40
- import { getVersion, normalizeDistTags } from '@verdaccio/utils';
43
+ import { createTarballHash, isObject, normalizeContributors } from '@verdaccio/utils';
41
44
 
42
- import { LocalStorage } from './local-storage';
43
- import { SearchManager } from './search';
44
- // import { isPublishablePackage, validateInputs } from './star-utils';
45
45
  import {
46
- checkPackageLocal,
47
- checkPackageRemote,
46
+ PublishOptions,
47
+ UpdateManifestOptions,
48
+ cleanUpReadme,
49
+ isDeprecatedManifest,
50
+ mapManifestToSearchPackageBody,
51
+ tagVersion,
52
+ tagVersionNext,
53
+ } from '.';
54
+ import { TransFormResults } from './lib/TransFormResults';
55
+ import { removeDuplicates } from './lib/search-utils';
56
+ import { isPublishablePackage } from './lib/star-utils';
57
+ import {
58
+ STORAGE,
48
59
  cleanUpLinksRef,
49
60
  generatePackageTemplate,
50
- mergeUplinkTimeIntoLocal,
51
- publishPackage,
52
- } from './storage-utils';
53
- import { IGetPackageOptions, IGetPackageOptionsNext, IPluginFilters, ISyncUplinks } from './type';
54
- // import { StarBody, Users } from './type';
55
- import { setupUpLinks, updateVersionsHiddenUpLink } from './uplink-util';
61
+ generateRevision,
62
+ getLatestReadme,
63
+ mergeUplinkTimeIntoLocalNext,
64
+ mergeVersions,
65
+ normalizeDistTags,
66
+ normalizePackage,
67
+ updateUpLinkMetadata,
68
+ } from './lib/storage-utils';
69
+ import { ProxyInstanceList, setupUpLinks, updateVersionsHiddenUpLinkNext } from './lib/uplink-util';
70
+ import { getVersion } from './lib/versions-utils';
71
+ import { LocalStorage } from './local-storage';
72
+ import { IGetPackageOptionsNext, IPluginFilters } from './type';
56
73
 
57
74
  const debug = buildDebug('verdaccio:storage');
75
+
76
+ export const noSuchFile = 'ENOENT';
77
+ export const resourceNotAvailable = 'EAGAIN';
78
+ export const PROTO_NAME = '__proto__';
79
+
58
80
  class Storage {
59
81
  public localStorage: LocalStorage;
60
- public searchManager: SearchManager | null;
82
+ public filters: IPluginFilters;
61
83
  public readonly config: Config;
62
84
  public readonly logger: Logger;
63
- public readonly uplinks: ProxyList;
64
- public filters: IPluginFilters;
65
-
85
+ public readonly uplinks: ProxyInstanceList;
66
86
  public constructor(config: Config) {
67
87
  this.config = config;
68
88
  this.uplinks = setupUpLinks(config);
69
- debug('uplinks available %o', Object.keys(this.uplinks));
70
89
  this.logger = logger.child({ module: 'storage' });
71
90
  this.filters = [];
72
91
  // @ts-ignore
73
92
  this.localStorage = null;
74
- this.searchManager = null;
75
- }
76
-
77
- public async init(config: Config, filters: IPluginFilters = []): Promise<void> {
78
- if (this.localStorage === null) {
79
- this.filters = filters || [];
80
- debug('filters available %o', filters);
81
- this.localStorage = new LocalStorage(this.config, logger);
82
- await this.localStorage.init();
83
- debug('local init storage initialized');
84
- await this.localStorage.getSecret(config);
85
- debug('local storage secret initialized');
86
- this.searchManager = new SearchManager(this.uplinks, this.localStorage);
87
- } else {
88
- debug('storage has been already initialized');
89
- }
90
- return;
91
- }
92
-
93
- /**
94
- * Add a {name} package to a system
95
- Function checks if package with the same name is available from uplinks.
96
- If it isn't, we create package locally
97
- Used storages: local (write) && uplinks
98
- */
99
- public async addPackage(name: string, metadata: any, callback: Function): Promise<void> {
100
- try {
101
- debug('add package for %o', name);
102
- await checkPackageLocal(name, this.localStorage);
103
- debug('look up remote for %o', name);
104
- await checkPackageRemote(
105
- name,
106
- this._isAllowPublishOffline(),
107
- this._syncUplinksMetadata.bind(this)
108
- );
109
- debug('publishing a package for %o', name);
110
- await publishPackage(name, metadata, this.localStorage as LocalStorage);
111
- // TODO: return published data and replace callback by a promise
112
- callback(null, true);
113
- } catch (err: any) {
114
- debug('error on add a package for %o with error %o', name, err);
115
- callback(err);
116
- }
117
- }
118
-
119
- /**
120
- * Add a {name} package to a system
121
- Function checks if package with the same name is available from uplinks.
122
- If it isn't, we create package locally
123
- Used storages: local (write) && uplinks
124
- */
125
- public async addPackageNext(name: string, metadata: Package): Promise<Package> {
126
- try {
127
- debug('add package for %o', name);
128
- await checkPackageLocal(name, this.localStorage);
129
- debug('look up remote for %o', name);
130
- await checkPackageRemote(
131
- name,
132
- this._isAllowPublishOffline(),
133
- this._syncUplinksMetadata.bind(this)
134
- );
135
- debug('publishing a package for %o', name);
136
- // FIXME: publishPackage should return fresh metadata from backend
137
- // instead return metadata
138
- await publishPackage(name, metadata, this.localStorage as LocalStorage);
139
- return metadata;
140
- } catch (err: any) {
141
- debug('error on add a package for %o with error %o', name, err);
142
- throw err;
143
- }
144
- }
145
-
146
- private _isAllowPublishOffline(): boolean {
147
- return (
148
- typeof this.config.publish !== 'undefined' &&
149
- _.isBoolean(this.config.publish.allow_offline) &&
150
- this.config.publish.allow_offline
151
- );
152
- }
153
-
154
- public readTokens(filter: TokenFilter): Promise<Token[]> {
155
- return this.localStorage.readTokens(filter);
156
- }
157
-
158
- public saveToken(token: Token): Promise<void> {
159
- return this.localStorage.saveToken(token);
160
- }
161
-
162
- public deleteToken(user: string, tokenKey: string): Promise<any> {
163
- return this.localStorage.deleteToken(user, tokenKey);
164
- }
165
-
166
- /**
167
- * Add a new version of package {name} to a system
168
- Used storages: local (write)
169
- */
170
- public addVersion(
171
- name: string,
172
- version: string,
173
- metadata: Version,
174
- tag: StringValue,
175
- callback: CallbackAction
176
- ): void {
177
- debug('add the version %o for package %o', version, name);
178
- this.localStorage.addVersion(name, version, metadata, tag, callback);
179
- }
180
-
181
- public async addVersionNext(
182
- name: string,
183
- version: string,
184
- metadata: Version,
185
- tag: StringValue
186
- ): Promise<void> {
187
- debug('add the version %o for package %o', version, name);
188
- return this.localStorage.addVersionNext(name, version, metadata, tag);
93
+ debug('uplinks available %o', Object.keys(this.uplinks));
189
94
  }
190
95
 
191
- /**
192
- * Tags a package version with a provided tag
193
- Used storages: local (write)
194
- */
195
- public mergeTags(name: string, tagHash: MergeTags, callback: CallbackAction): void {
196
- debug('merge tags for package %o tags %o', name, tagHash);
197
- this.localStorage.mergeTags(name, tagHash, callback);
198
- }
96
+ static ABBREVIATED_HEADER =
97
+ 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*';
199
98
 
200
99
  /**
201
100
  * Change an existing package (i.e. unpublish one version)
202
101
  Function changes a package info from local storage and all uplinks with write access./
203
102
  Used storages: local (write)
204
103
  */
205
- public changePackage(
104
+ public async changePackageNext(
206
105
  name: string,
207
- metadata: Package,
208
- revision: string,
209
- callback: Callback
210
- ): void {
106
+ metadata: Manifest,
107
+ revision: string
108
+ ): Promise<void> {
211
109
  debug('change existing package for package %o revision %o', name, revision);
212
- this.localStorage.changePackage(name, metadata, revision, callback);
213
- }
110
+ debug(`change manifest tags for %o revision %s`, name, revision);
111
+ if (
112
+ !validatioUtils.isObject(metadata.versions) ||
113
+ !validatioUtils.isObject(metadata[DIST_TAGS])
114
+ ) {
115
+ debug(`change manifest bad data for %o`, name);
116
+ throw errorUtils.getBadData();
117
+ }
214
118
 
215
- /**
216
- * Change an existing package (i.e. unpublish one version)
217
- Function changes a package info from local storage and all uplinks with write access./
218
- Used storages: local (write)
219
- */
220
- public async changePackageNext(name: string, metadata: Package, revision: string): Promise<void> {
221
- debug('change existing package for package %o revision %o', name, revision);
222
- this.localStorage.changePackageNext(name, metadata, revision);
119
+ debug(`change manifest udapting manifest for %o`, name);
120
+ await this.updatePackageNext(name, async (localData: Manifest): Promise<Manifest> => {
121
+ // eslint-disable-next-line guard-for-in
122
+ for (const version in localData.versions) {
123
+ const incomingVersion = metadata.versions[version];
124
+ if (_.isNil(incomingVersion)) {
125
+ this.logger.info({ name: name, version: version }, 'unpublishing @{name}@@{version}');
126
+
127
+ // FIXME: I prefer return a new object rather mutate the metadata
128
+ delete localData.versions[version];
129
+ delete localData.time![version];
130
+
131
+ for (const file in localData._attachments) {
132
+ if (localData._attachments[file].version === version) {
133
+ delete localData._attachments[file].version;
134
+ }
135
+ }
136
+ } else if (Object.prototype.hasOwnProperty.call(incomingVersion, 'deprecated')) {
137
+ const incomingDeprecated = incomingVersion.deprecated;
138
+ if (incomingDeprecated != localData.versions[version].deprecated) {
139
+ if (!incomingDeprecated) {
140
+ this.logger.info(
141
+ { name: name, version: version },
142
+ 'undeprecating @{name}@@{version}'
143
+ );
144
+ delete localData.versions[version].deprecated;
145
+ } else {
146
+ this.logger.info({ name: name, version: version }, 'deprecating @{name}@@{version}');
147
+ localData.versions[version].deprecated = incomingDeprecated;
148
+ }
149
+ localData.time!.modified = new Date().toISOString();
150
+ }
151
+ }
152
+ }
153
+
154
+ localData[USERS] = metadata[USERS];
155
+ localData[DIST_TAGS] = metadata[DIST_TAGS];
156
+ return localData;
157
+ });
223
158
  }
224
159
 
225
- /**
226
- * Remove a package from a system
227
- Function removes a package from local storage
228
- Used storages: local (write)
229
- */
230
- public async removePackage(name: string): Promise<void> {
231
- debug('remove packagefor package %o', name);
232
- await this.localStorage.removePackage(name);
160
+ public async removePackage(name: string, revision): Promise<void> {
161
+ debug('remove package %o', name);
162
+ await this.removePackageByRevision(name, revision);
233
163
  }
234
164
 
235
165
  /**
@@ -239,208 +169,300 @@ class Storage {
239
169
  versions, i.e. package version should be unpublished first.
240
170
  Used storage: local (write)
241
171
  */
242
- public removeTarball(
243
- name: string,
244
- filename: string,
245
- revision: string,
246
- callback: CallbackAction
247
- ): void {
248
- this.localStorage.removeTarball(name, filename, revision, callback);
249
- }
172
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
173
+ public async removeTarball(name: string, filename: string, _revision: string): Promise<Manifest> {
174
+ debug('remove tarball %s for %s', filename, name);
175
+ assert(validatioUtils.validateName(filename));
176
+ const storage: IPackageStorage = this.getPrivatePackageStorage(name);
177
+ if (!storage) {
178
+ debug(`method not implemented for storage`);
179
+ this.logger.error('method for remove tarball not implemented');
180
+ throw errorUtils.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR);
181
+ }
250
182
 
251
- /**
252
- * Upload a tarball for {name} package
253
- Function is synchronous and returns a WritableStream
254
- Used storages: local (write)
255
- */
256
- public addTarball(name: string, filename: string): IUploadTarball {
257
- debug('add tarball for package %o', name);
258
- return this.localStorage.addTarball(name, filename);
183
+ try {
184
+ const cacheManifest = await storage.readPackage(name);
185
+ if (!cacheManifest._attachments[filename]) {
186
+ throw errorUtils.getNotFound('no such file available');
187
+ }
188
+ } catch (err: any) {
189
+ if (err.code === noSuchFile) {
190
+ throw errorUtils.getNotFound();
191
+ }
192
+ throw err;
193
+ }
194
+
195
+ const manifest = await this.updatePackageNext(
196
+ name,
197
+ async (data: Manifest): Promise<Manifest> => {
198
+ let newData: Manifest = { ...data };
199
+ delete data._attachments[filename];
200
+ return newData;
201
+ }
202
+ );
203
+
204
+ try {
205
+ const storage: IPackageStorage = this.getPrivatePackageStorage(name);
206
+ if (!storage) {
207
+ debug(`method not implemented for storage`);
208
+ this.logger.error('method for remove tarball not implemented');
209
+ throw errorUtils.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR);
210
+ }
211
+
212
+ await storage.deletePackage(filename);
213
+ debug('package %s removed', filename);
214
+ } catch (err: any) {
215
+ this.logger.error({ err }, 'error removing %s from storage');
216
+ throw err;
217
+ }
218
+ return manifest;
259
219
  }
260
220
 
261
221
  /**
262
- Get a tarball from a storage for {name} package
263
- Function is synchronous and returns a ReadableStream
264
- Function tries to read tarball locally, if it fails then it reads package
265
- information in order to figure out where we can get this tarball from
266
- Used storages: local || uplink (just one)
222
+ * Handle search on packages and proxies.
223
+ * Iterate all proxies configured and search in all endpoints in v2 and pipe all responses
224
+ * to a stream, once the proxies request has finished search in local storage for all packages
225
+ * (privated and cached).
267
226
  */
268
- public getTarball(name: string, filename: string): IReadTarball {
269
- debug('get tarball for package %o filename %o', name, filename);
270
- const readStream = new ReadTarball({});
271
- readStream.abort = function () {};
227
+ public async search(options: ProxySearchParams): Promise<searchUtils.SearchPackageItem[]> {
228
+ const transformResults = new TransFormResults({ objectMode: true });
229
+ const streamPassThrough = new PassThrough({ objectMode: true });
230
+ const upLinkList = this.getProxyList();
231
+
232
+ const searchUplinksStreams = upLinkList.map((uplinkId: string) => {
233
+ const uplink = this.uplinks[uplinkId];
234
+ if (!uplink) {
235
+ // this should never tecnically happens
236
+ this.logger.error({ uplinkId }, 'uplink @upLinkId not found');
237
+ }
238
+ return this.consumeSearchStream(uplinkId, uplink, options, streamPassThrough);
239
+ });
272
240
 
273
- const self = this;
241
+ try {
242
+ debug('search uplinks');
243
+ // we only process those streams end successfully, if all fails
244
+ // we just include local storage
245
+ await Promise.allSettled([...searchUplinksStreams]);
246
+ debug('search uplinks done');
247
+ } catch (err: any) {
248
+ this.logger.error({ err: err?.message }, ' error on uplinks search @{err}');
249
+ streamPassThrough.emit('error', err);
250
+ }
251
+ debug('search local');
252
+ try {
253
+ await this.searchCachedPackages(streamPassThrough, options.query as searchUtils.SearchQuery);
254
+ } catch (err: any) {
255
+ this.logger.error({ err: err?.message }, ' error on local search @{err}');
256
+ streamPassThrough.emit('error', err);
257
+ }
258
+ const data: searchUtils.SearchPackageItem[] = [];
259
+ const outPutStream = new PassThrough({ objectMode: true });
260
+ streamPipeline(streamPassThrough, transformResults, outPutStream, (err: any) => {
261
+ if (err) {
262
+ this.logger.error({ err: err?.message }, ' error on search @{err}');
263
+ throw errorUtils.getInternalError(err ? err.message : 'unknown search error');
264
+ } else {
265
+ debug('pipeline succeeded');
266
+ }
267
+ });
274
268
 
275
- // if someone requesting tarball, it means that we should already have some
276
- // information about it, so fetching package info is unnecessary
269
+ outPutStream.on('data', (chunk) => {
270
+ data.push(chunk);
271
+ });
277
272
 
278
- // trying local first
279
- // flow: should be IReadTarball
280
- let localStream: any = self.localStorage.getTarball(name, filename);
281
- let isOpen = false;
282
- localStream.on('error', (err): any => {
283
- if (isOpen || err.status !== HTTP_STATUS.NOT_FOUND) {
284
- return readStream.emit('error', err);
285
- }
286
-
287
- // local reported 404
288
- const err404 = err;
289
- localStream.abort();
290
- localStream = null; // we force for garbage collector
291
- self.localStorage.getPackageMetadata(name, (err, info: Package): void => {
292
- if (_.isNil(err) && info._distfiles && _.isNil(info._distfiles[filename]) === false) {
293
- // information about this file exists locally
294
- serveFile(info._distfiles[filename]);
295
- } else {
296
- // we know nothing about this file, trying to get information elsewhere
297
- self._syncUplinksMetadata(name, info, {}, (err, info: Package): any => {
298
- if (_.isNil(err) === false) {
299
- return readStream.emit('error', err);
300
- }
301
- if (_.isNil(info._distfiles) || _.isNil(info._distfiles[filename])) {
302
- return readStream.emit('error', err404);
303
- }
304
- serveFile(info._distfiles[filename]);
305
- });
306
- }
273
+ return new Promise((resolve) => {
274
+ outPutStream.on('finish', async () => {
275
+ const searchFinalResults: searchUtils.SearchPackageItem[] = removeDuplicates(data);
276
+ debug('search stream total results: %o', searchFinalResults.length);
277
+ return resolve(searchFinalResults);
307
278
  });
279
+ debug('search done');
308
280
  });
309
- localStream.on('content-length', function (v): void {
310
- readStream.emit('content-length', v);
311
- });
281
+ }
312
282
 
313
- localStream.on('open', function (): void {
314
- isOpen = true;
315
- localStream.pipe(readStream);
316
- });
317
- return readStream;
318
-
319
- /**
320
- * Fetch and cache local/remote packages.
321
- * @param {Object} file define the package shape
322
- */
323
- function serveFile(file: DistFile): void {
324
- let uplink: any = null;
325
-
326
- for (const uplinkId in self.uplinks) {
327
- // https://github.com/verdaccio/verdaccio/issues/1642
328
- if (hasProxyTo(name, uplinkId, self.config.packages)) {
329
- uplink = self.uplinks[uplinkId];
283
+ private async getTarballFromUpstream(name: string, filename: string, { signal }) {
284
+ let cachedManifest: Manifest | null = null;
285
+ try {
286
+ cachedManifest = await this.getPackageLocalMetadata(name);
287
+ } catch (err) {
288
+ debug('error on get package local metadata %o', err);
289
+ }
290
+ // dist url should be on local cache metadata
291
+ if (
292
+ cachedManifest?._distfiles &&
293
+ typeof cachedManifest?._distfiles[filename]?.url === 'string'
294
+ ) {
295
+ debug('dist file found, using it %o', cachedManifest?._distfiles[filename].url);
296
+ // dist file found, proceed to download
297
+ const distFile = cachedManifest._distfiles[filename];
298
+
299
+ let current_length = 0;
300
+ let expected_length;
301
+ const passThroughRemoteStream = new PassThrough();
302
+ const proxy = this.getUpLinkForDistFile(name, distFile);
303
+ const remoteStream = proxy.fetchTarballNext(distFile.url, {});
304
+
305
+ remoteStream.on('request', async () => {
306
+ try {
307
+ debug('remote stream request');
308
+ const storage = this.getPrivatePackageStorage(name) as any;
309
+ if (proxy.config.cache === true && storage) {
310
+ const localStorageWriteStream = await storage.writeTarball(filename, {
311
+ signal,
312
+ });
313
+
314
+ await pipeline(remoteStream, passThroughRemoteStream, localStorageWriteStream, {
315
+ signal,
316
+ });
317
+ } else {
318
+ await pipeline(remoteStream, passThroughRemoteStream, {
319
+ signal,
320
+ });
321
+ }
322
+ } catch (err: any) {
323
+ debug('error on pipeline downloading tarball for package %o', name);
324
+ passThroughRemoteStream.emit('error', err);
330
325
  }
331
- }
326
+ });
332
327
 
333
- if (uplink == null) {
334
- uplink = new ProxyStorage(
335
- {
336
- url: file.url,
337
- cache: true,
338
- _autogenerated: true,
339
- },
340
- self.config
341
- );
342
- }
328
+ remoteStream
329
+ .on('response', async (res) => {
330
+ if (res.statusCode === HTTP_STATUS.NOT_FOUND) {
331
+ debug('remote stream response 404');
332
+ passThroughRemoteStream.emit(
333
+ 'error',
334
+ errorUtils.getNotFound(errorUtils.API_ERROR.NOT_FILE_UPLINK)
335
+ );
336
+ return;
337
+ }
343
338
 
344
- let savestream: IUploadTarball | null = null;
345
- if (uplink.config.cache) {
346
- savestream = self.localStorage.addTarball(name, filename);
347
- }
339
+ if (
340
+ !(res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)
341
+ ) {
342
+ debug('remote stream response ok');
343
+ passThroughRemoteStream.emit(
344
+ 'error',
345
+ errorUtils.getInternalError(`bad uplink status code: ${res.statusCode}`)
346
+ );
347
+ return;
348
+ }
348
349
 
349
- let on_open = function (): void {
350
- // prevent it from being called twice
351
- on_open = function () {};
352
- const rstream2 = uplink.fetchTarball(file.url);
353
- rstream2.on('error', function (err): void {
354
- if (savestream) {
355
- savestream.abort();
350
+ if (res.headers[HEADER_TYPE.CONTENT_LENGTH]) {
351
+ expected_length = res.headers[HEADER_TYPE.CONTENT_LENGTH];
352
+ debug('remote stream response content length %o', expected_length);
353
+ passThroughRemoteStream.emit(
354
+ HEADER_TYPE.CONTENT_LENGTH,
355
+ res.headers[HEADER_TYPE.CONTENT_LENGTH]
356
+ );
356
357
  }
357
- savestream = null;
358
- readStream.emit('error', err);
359
- });
360
- rstream2.on('end', function (): void {
361
- if (savestream) {
362
- savestream.done();
358
+ })
359
+ .on('downloadProgress', (progress) => {
360
+ current_length = progress.transferred;
361
+ if (typeof expected_length === 'undefined' && progress.total) {
362
+ expected_length = progress.total;
363
+ }
364
+ })
365
+ .on('end', () => {
366
+ if (expected_length && current_length != expected_length) {
367
+ debug('stream end, but length mismatch %o %o', current_length, expected_length);
368
+ passThroughRemoteStream.emit(
369
+ 'error',
370
+ errorUtils.getInternalError(API_ERROR.CONTENT_MISMATCH)
371
+ );
363
372
  }
373
+ debug('remote stream end');
374
+ })
375
+ .on('error', (err) => {
376
+ debug('remote stream error %o', err);
377
+ passThroughRemoteStream.emit('error', err);
364
378
  });
379
+ return passThroughRemoteStream;
380
+ } else {
381
+ debug('dist file not found, proceed update upstream');
382
+ // no dist url found, proceed to fetch from upstream
383
+ // should not be the case
384
+ const passThroughRemoteStream = new PassThrough();
385
+ // ensure get the latest data
386
+ const [updatedManifest] = await this.syncUplinksMetadataNext(name, cachedManifest, {
387
+ uplinksLook: true,
388
+ });
389
+ const distFile = (updatedManifest as Manifest)._distfiles[filename];
365
390
 
366
- rstream2.on('content-length', function (v): void {
367
- readStream.emit('content-length', v);
368
- if (savestream) {
369
- savestream.emit('content-length', v);
391
+ if (updatedManifest === null || !distFile) {
392
+ debug('remote tarball not found');
393
+ throw errorUtils.getNotFound(API_ERROR.NO_SUCH_FILE);
394
+ }
395
+
396
+ const proxy = this.getUpLinkForDistFile(name, distFile);
397
+ const remoteStream = proxy.fetchTarballNext(distFile.url, {});
398
+ remoteStream.on('response', async () => {
399
+ try {
400
+ const storage = this.getPrivatePackageStorage(name);
401
+ if (proxy.config.cache === true && storage) {
402
+ debug('cache remote tarball enabled');
403
+ const localStorageWriteStream = await storage.writeTarball(filename, {
404
+ signal,
405
+ });
406
+ await pipeline(remoteStream, passThroughRemoteStream, localStorageWriteStream, {
407
+ signal,
408
+ });
409
+ } else {
410
+ debug('cache remote tarball disabled');
411
+ await pipeline(remoteStream, passThroughRemoteStream, { signal });
370
412
  }
371
- });
372
- rstream2.pipe(readStream);
373
- if (savestream) {
374
- rstream2.pipe(savestream);
413
+ } catch (err) {
414
+ debug('error on pipeline downloading tarball for package %o', name);
415
+ passThroughRemoteStream.emit('error', err);
375
416
  }
376
- };
417
+ });
418
+ return passThroughRemoteStream;
419
+ }
420
+ }
377
421
 
378
- if (savestream) {
379
- savestream.on('open', function (): void {
380
- on_open();
381
- });
422
+ /**
423
+ *
424
+ * @param name
425
+ * @param filename
426
+ * @param param2
427
+ * @returns
428
+ */
429
+ public async getTarballNext(name: string, filename: string, { signal }): Promise<PassThrough> {
430
+ debug('get tarball for package %o filename %o', name, filename);
431
+ // TODO: check if isOpen is need it after all.
432
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
433
+ let isOpen = false;
434
+ const localTarballStream = new PassThrough();
435
+ const localStream = await this.getLocalTarball(name, filename, { signal });
436
+ localStream.on('open', async () => {
437
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
438
+ isOpen = true;
439
+ await pipeline(localStream, localTarballStream, { signal });
440
+ });
382
441
 
383
- savestream.on('error', function (err): void {
384
- self.logger.warn(
385
- { err: err, fileName: file },
386
- 'error saving file @{fileName}: @{err?.message}\n@{err.stack}'
387
- );
388
- if (savestream) {
389
- savestream.abort();
390
- }
391
- savestream = null;
392
- on_open();
393
- });
442
+ localStream.on('error', (err: any) => {
443
+ // eslint-disable-next-line no-console
444
+ if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
445
+ this.getTarballFromUpstream(name, filename, { signal })
446
+ .then((uplinkStream) => {
447
+ pipeline(uplinkStream, localTarballStream, { signal })
448
+ .then(() => {
449
+ debug('successfully downloaded tarball for package %o filename %o', name, filename);
450
+ })
451
+ .catch((err) => {
452
+ localTarballStream.emit('error', err);
453
+ });
454
+ })
455
+ .catch((err) => {
456
+ localTarballStream.emit('error', err);
457
+ });
394
458
  } else {
395
- on_open();
396
- }
397
- }
398
- }
399
-
400
- // public async starPackage(body: StarBody, options: IGetPackageOptionsNext): Promise<void> {
401
- // debug('star package');
402
- // const manifest = await this.getPackageNext(options);
403
- // const newStarUser = body[constants.USERS];
404
- // const remoteUsername: string = options.remoteUser.name as string;
405
- // const localStarUsers = manifest[constants.USERS];
406
- // // Check is star or unstar
407
- // const isStar = Object.keys(newStarUser).includes(remoteUsername);
408
- // debug('is start? %o', isStar);
409
- // if (
410
- // _.isNil(localStarUsers) === false &&
411
- // validateInputs(localStarUsers, remoteUsername, isStar)
412
- // ) {
413
- // // return afterChangePackage();
414
- // }
415
- // const users: Users = isStar
416
- // ? {
417
- // ...localStarUsers,
418
- // [remoteUsername]: true,
419
- // }
420
- // : _.reduce(
421
- // localStarUsers,
422
- // (users, value, key) => {
423
- // if (key !== remoteUsername) {
424
- // users[key] = value;
425
- // }
426
- // return users;
427
- // },
428
- // {}
429
- // );
430
- // debug('update package for %o', name);
431
- // }
432
-
433
- // public async publish(body: any, options: IGetPackageOptionsNext): Promise<any> {
434
- // const { name } = options;
435
- // debug('publishing or updating a new version for %o', name);
436
- // // we check if the request is npm star
437
- // if (!isPublishablePackage(body) && isObject(body.users)) {
438
- // debug('starting star a package');
439
- // await this.starPackage(body as StarBody, options);
440
- // }
441
-
442
- // return { ok: API_MESSAGE.PKG_CHANGED, success: true };
443
- // }
459
+ this.logger.error({ err: err.message }, 'some error on fatal @{err}');
460
+ localTarballStream.emit('error', err);
461
+ }
462
+ });
463
+
464
+ return localTarballStream;
465
+ }
444
466
 
445
467
  public async getPackageByVersion(options: IGetPackageOptionsNext): Promise<Version> {
446
468
  const queryVersion = options.version as string;
@@ -494,7 +516,7 @@ class Storage {
494
516
  throw errorUtils.getNotFound(`${API_ERROR.VERSION_NOT_EXIST}: ${queryVersion}`);
495
517
  }
496
518
 
497
- public async getPackageManifest(options: IGetPackageOptionsNext): Promise<Package> {
519
+ public async getPackageManifest(options: IGetPackageOptionsNext): Promise<Manifest> {
498
520
  // convert dist remotes to local bars
499
521
  const [manifest] = await this.getPackageNext(options);
500
522
  const convertedManifest = convertDistRemoteToLocalTarballUrls(
@@ -506,362 +528,1388 @@ class Storage {
506
528
  return convertedManifest;
507
529
  }
508
530
 
531
+ private convertAbbreviatedManifest(manifest: Manifest): AbbreviatedManifest {
532
+ const abbreviatedVersions = Object.keys(manifest.versions).reduce(
533
+ (acc: AbbreviatedVersions, version: string) => {
534
+ const _version = manifest.versions[version];
535
+ // This should be align with this document
536
+ // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
537
+ const _version_abbreviated = {
538
+ name: _version.name,
539
+ version: _version.version,
540
+ description: _version.description,
541
+ deprecated: _version.deprecated,
542
+ bin: _version.bin,
543
+ dist: _version.dist,
544
+ engines: _version.engines,
545
+ funding: _version.funding,
546
+ directories: _version.directories,
547
+ dependencies: _version.dependencies,
548
+ devDependencies: _version.devDependencies,
549
+ peerDependencies: _version.peerDependencies,
550
+ optionalDependencies: _version.optionalDependencies,
551
+ bundleDependencies: _version.bundleDependencies,
552
+ // npm cli specifics
553
+ _hasShrinkwrap: _version._hasShrinkwrap,
554
+ hasInstallScript: _version.hasInstallScript,
555
+ };
556
+ acc[version] = _version_abbreviated;
557
+ return acc;
558
+ },
559
+ {}
560
+ );
561
+ const convertedManifest = {
562
+ name: manifest['name'],
563
+ [DIST_TAGS]: manifest[DIST_TAGS],
564
+ versions: abbreviatedVersions,
565
+ modified: manifest.time.modified,
566
+ // NOTE: special case for pnpm https://github.com/pnpm/rfcs/pull/2
567
+ time: manifest.time,
568
+ };
569
+
570
+ return convertedManifest;
571
+ }
572
+
509
573
  /**
510
574
  * Return a manifest or version based on the options.
511
575
  * @param options {Object}
512
576
  * @returns A package manifest or specific version
513
577
  */
514
- public async getPackageByOptions(options: IGetPackageOptionsNext): Promise<Package | Version> {
578
+ public async getPackageByOptions(
579
+ options: IGetPackageOptionsNext
580
+ ): Promise<Manifest | AbbreviatedManifest | Version> {
515
581
  // if no version we return the whole manifest
516
582
  if (_.isNil(options.version) === false) {
517
583
  return this.getPackageByVersion(options);
518
584
  } else {
519
- return this.getPackageManifest(options);
585
+ const manifest = await this.getPackageManifest(options);
586
+ if (options.abbreviated === true) {
587
+ debug('abbreviated manifest');
588
+ return this.convertAbbreviatedManifest(manifest);
589
+ }
590
+ return manifest;
520
591
  }
521
592
  }
522
593
 
523
- public async getPackageNext(options: IGetPackageOptionsNext): Promise<[Package, any[]]> {
524
- const { name } = options;
525
- debug('get package for %o', name);
526
- try {
527
- let data: Package;
528
- try {
529
- data = await this.localStorage.getPackageMetadataNext(name);
530
- } catch (err: any) {
531
- // we don't have package locally, so we need to fetch it from uplinks
532
- if (err && (!err.status || err.status >= HTTP_STATUS.INTERNAL_ERROR)) {
533
- throw err;
534
- }
594
+ public async getLocalDatabaseNext(): Promise<Version[]> {
595
+ debug('get local database');
596
+ const storage = this.localStorage.getStoragePlugin();
597
+ const database = await storage.get();
598
+ const packages: Version[] = [];
599
+ for (const pkg of database) {
600
+ debug('get local database %o', pkg);
601
+ const manifest = await this.getPackageLocalMetadata(pkg);
602
+ const latest = manifest[DIST_TAGS].latest;
603
+ if (latest && manifest.versions[latest]) {
604
+ const version: Version = manifest.versions[latest];
605
+ const timeList = manifest.time as GenericBody;
606
+ const time = timeList[latest];
607
+ // @ts-ignore
608
+ version.time = time;
609
+
610
+ // Add for stars api
611
+ // @ts-ignore
612
+ version.users = manifest.users;
613
+
614
+ packages.push(version);
615
+ } else {
616
+ this.logger.warn({ package: pkg }, 'package @{package} does not have a "latest" tag?');
535
617
  }
536
-
537
- // time to sync with uplinks if we have any
538
- debug('sync uplinks for %o', name);
539
- // @ts-expect-error
540
- return this._syncUplinksMetadataNext(name, data, {
541
- req: options.req,
542
- uplinksLook: options.uplinksLook,
543
- keepUpLinkData: options.keepUpLinkData,
544
- });
545
- } catch (err: any) {
546
- this.logger.error(
547
- { name, err: err.message },
548
- 'error on get package for @{name} with error @{err}'
549
- );
550
- throw err;
551
618
  }
619
+ return packages;
552
620
  }
553
621
 
554
- private _syncUplinksMetadataNext(
555
- name: string,
556
- data: Package,
557
- { uplinksLook, keepUpLinkData, req }
558
- ): Promise<[Package, any[]]> {
559
- return new Promise((resolve, reject) => {
560
- this._syncUplinksMetadata(
561
- name,
562
- data,
563
- { req: req, uplinksLook },
564
- function getPackageSynUpLinksCallback(err, result: Package, uplinkErrors): void {
565
- if (err) {
566
- debug('error on sync package for %o with error %o', name, err?.message);
567
- return reject(err);
568
- }
569
-
570
- const normalizedPkg = Object.assign({}, result, {
571
- ...normalizeDistTags(cleanUpLinksRef(result, keepUpLinkData)),
572
- _attachments: {},
573
- });
574
-
575
- debug('no. sync uplinks errors %o for %s', uplinkErrors?.length, name);
576
- resolve([normalizedPkg, uplinkErrors]);
577
- }
622
+ public saveToken(token: Token): Promise<any> {
623
+ if (_.isFunction(this.localStorage.getStoragePlugin().saveToken) === false) {
624
+ return Promise.reject(
625
+ errorUtils.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
578
626
  );
579
- });
580
- }
627
+ }
581
628
 
582
- /**
583
- Retrieve a package metadata for {name} package
584
- Function invokes localStorage.getPackage and uplink.get_package for every
585
- uplink with proxy_access rights against {name} and combines results
586
- into one json object
587
- Used storages: local && uplink (proxy_access)
588
-
589
- * @param {object} options
590
- * @property {string} options.name Package Name
591
- * @property {object} options.req Express `req` object
592
- * @property {boolean} options.keepUpLinkData keep up link info in package meta, last update, etc.
593
- * @property {function} options.callback Callback for receive data
594
- */
595
- public getPackage(options: IGetPackageOptions): void {
596
- const { name } = options;
597
- debug('get package for %o', name);
598
- this.localStorage.getPackageMetadata(name, (err, data) => {
599
- if (err && (!err.status || err.status >= HTTP_STATUS.INTERNAL_ERROR)) {
600
- // report internal errors right away
601
- debug('error on get package for %o with error %o', name, err?.message);
602
- return options.callback(err);
603
- }
604
-
605
- debug('sync uplinks for %o', name);
606
- this._syncUplinksMetadata(
607
- name,
608
- data,
609
- { req: options.req, uplinksLook: options.uplinksLook },
610
- function getPackageSynUpLinksCallback(err, result: Package, uplinkErrors): void {
611
- if (err) {
612
- debug('error on sync package for %o with error %o', name, err?.message);
613
- return options.callback(err);
614
- }
629
+ return this.localStorage.getStoragePlugin().saveToken(token);
630
+ }
615
631
 
616
- result = normalizeDistTags(cleanUpLinksRef(result, options?.keepUpLinkData));
632
+ public deleteToken(user: string, tokenKey: string): Promise<any> {
633
+ if (_.isFunction(this.localStorage.getStoragePlugin().deleteToken) === false) {
634
+ return Promise.reject(
635
+ errorUtils.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
636
+ );
637
+ }
617
638
 
618
- // npm can throw if this field doesn't exist
619
- result._attachments = {};
639
+ return this.localStorage.getStoragePlugin().deleteToken(user, tokenKey);
640
+ }
620
641
 
621
- debug('no. sync uplinks errors %o', uplinkErrors?.length);
622
- options.callback(null, result, uplinkErrors);
623
- }
642
+ public readTokens(filter: TokenFilter): Promise<Token[]> {
643
+ if (_.isFunction(this.localStorage.getStoragePlugin().readTokens) === false) {
644
+ return Promise.reject(
645
+ errorUtils.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE)
624
646
  );
625
- });
647
+ }
648
+
649
+ return this.localStorage.getStoragePlugin().readTokens(filter);
626
650
  }
627
651
 
628
652
  /**
629
- * Retrieve only private local packages
630
- * @param {*} callback
653
+ * Initialize the storage asynchronously.
654
+ * @param config Config
655
+ * @param filters IPluginFilters
656
+ * @returns Storage instance
631
657
  */
632
- public getLocalDatabase(callback: Callback): void {
633
- const self = this;
634
- debug('get local database');
635
- if (this.localStorage.storagePlugin !== null) {
636
- this.localStorage.storagePlugin
637
- .get()
638
- .then((locals) => {
639
- const packages: Version[] = [];
640
- const getPackage = function (itemPkg): void {
641
- self.localStorage.getPackageMetadata(
642
- locals[itemPkg],
643
- function (err, pkgMetadata: Package): void {
644
- if (_.isNil(err)) {
645
- const latest = pkgMetadata[DIST_TAGS].latest;
646
- if (latest && pkgMetadata.versions[latest]) {
647
- const version: Version = pkgMetadata.versions[latest];
648
- const timeList = pkgMetadata.time as GenericBody;
649
- const time = timeList[latest];
650
- // @ts-ignore
651
- version.time = time;
652
-
653
- // Add for stars api
654
- // @ts-ignore
655
- version.users = pkgMetadata.users;
656
-
657
- packages.push(version);
658
- } else {
659
- self.logger.warn(
660
- { package: locals[itemPkg] },
661
- 'package @{package} does not have a "latest" tag?'
662
- );
663
- }
664
- }
665
-
666
- if (itemPkg >= locals.length - 1) {
667
- callback(null, packages);
668
- } else {
669
- getPackage(itemPkg + 1);
670
- }
671
- }
672
- );
673
- };
674
-
675
- if (locals.length) {
676
- getPackage(0);
677
- } else {
678
- callback(null, []);
679
- }
680
- })
681
- .catch((err) => {
682
- callback(err);
683
- });
658
+ public async init(config: Config, filters: IPluginFilters = []): Promise<void> {
659
+ if (this.localStorage === null) {
660
+ this.filters = filters || [];
661
+ debug('filters available %o', filters);
662
+ this.localStorage = new LocalStorage(this.config, logger);
663
+ await this.localStorage.init();
664
+ debug('local init storage initialized');
665
+ await this.localStorage.getSecret(config);
666
+ debug('local storage secret initialized');
684
667
  } else {
685
- debug('local stora instance is null');
668
+ debug('storage has been already initialized');
686
669
  }
670
+ return;
687
671
  }
688
672
 
689
- // notas, debo migrar _syncUplinksMetadata algo mas lindo
690
-
691
673
  /**
692
- * Function fetches package metadata from uplinks and synchronizes it with local data
693
- if package is available locally, it MUST be provided in pkginfo
694
- returns callback(err, result, uplink_errors)
674
+ * Consume the upstream and pipe it to a transformable stream.
695
675
  */
696
- public _syncUplinksMetadata(
697
- name: string,
698
- packageInfo: Package,
699
- options: ISyncUplinks,
700
- callback: Callback
701
- ): void {
702
- let found = true;
703
- const self = this;
704
- const upLinks: IProxy[] = [];
705
- const hasToLookIntoUplinks = _.isNil(options.uplinksLook) || options.uplinksLook;
706
- debug('is sync uplink enabled %o', hasToLookIntoUplinks);
676
+ private consumeSearchStream(
677
+ uplinkId: string,
678
+ uplink: IProxy,
679
+ options: ProxySearchParams,
680
+ searchPassThrough: PassThrough
681
+ ): Promise<any> {
682
+ return uplink.search({ ...options }).then((bodyStream) => {
683
+ bodyStream.pipe(searchPassThrough, { end: false });
684
+ bodyStream.on('error', (err: any): void => {
685
+ logger.error(
686
+ { uplinkId, err: err },
687
+ 'search error for uplink @{uplinkId}: @{err?.message}'
688
+ );
689
+ searchPassThrough.end();
690
+ });
691
+ return new Promise((resolve) => bodyStream.on('end', resolve));
692
+ });
693
+ }
707
694
 
708
- if (!packageInfo) {
709
- found = false;
710
- packageInfo = generatePackageTemplate(name);
695
+ /**
696
+ * Retrieve a wrapper that provide access to the package location.
697
+ * @param {Object} pkgName package name.
698
+ * @return {Object}
699
+ */
700
+ private getPrivatePackageStorage(pkgName: string): IPackageStorage {
701
+ debug('get local storage for %o', pkgName);
702
+ return this.localStorage.getStoragePlugin().getPackageStorage(pkgName);
703
+ }
704
+
705
+ /**
706
+ * Create a tarball stream from a package.
707
+ * @param name
708
+ * @param filename
709
+ * @param options
710
+ * @returns
711
+ */
712
+ public async getLocalTarball(
713
+ pkgName: string,
714
+ filename: string,
715
+ { signal }: { signal: AbortSignal }
716
+ ): Promise<Readable> {
717
+ assert(validatioUtils.validateName(filename));
718
+ const storage: IPackageStorage = this.getPrivatePackageStorage(pkgName);
719
+ if (typeof storage === 'undefined') {
720
+ return this.createFailureStreamResponseNext();
711
721
  }
712
722
 
713
- for (const uplink in this.uplinks) {
714
- if (hasProxyTo(name, uplink, this.config.packages) && hasToLookIntoUplinks) {
715
- upLinks.push(this.uplinks[uplink]);
723
+ return await storage.readTarball(filename, { signal });
724
+ }
725
+
726
+ private async searchCachedPackages(
727
+ searchStream: PassThrough,
728
+ query: searchUtils.SearchQuery
729
+ ): Promise<void> {
730
+ debug('search on each package');
731
+ this.logger.info(
732
+ { t: query.text, q: query.quality, p: query.popularity, m: query.maintenance, s: query.size },
733
+ 'search by text @{t}| maintenance @{m}| quality @{q}| popularity @{p}'
734
+ );
735
+
736
+ if (typeof this.localStorage.getStoragePlugin().search === 'undefined') {
737
+ this.logger.info('plugin search not implemented yet');
738
+ searchStream.end();
739
+ } else {
740
+ debug('search on each package by plugin');
741
+ const items = await this.localStorage.getStoragePlugin().search(query);
742
+ try {
743
+ for (const searchItem of items) {
744
+ const manifest = await this.getPackageLocalMetadata(searchItem.package.name);
745
+ if (_.isEmpty(manifest?.versions) === false) {
746
+ const searchPackage = mapManifestToSearchPackageBody(manifest, searchItem);
747
+ const searchPackageItem: searchUtils.SearchPackageItem = {
748
+ package: searchPackage,
749
+ score: searchItem.score,
750
+ verdaccioPkgCached: searchItem.verdaccioPkgCached,
751
+ verdaccioPrivate: searchItem.verdaccioPrivate,
752
+ flags: searchItem?.flags,
753
+ // FUTURE: find a better way to calculate the score
754
+ searchScore: 1,
755
+ };
756
+ searchStream.write(searchPackageItem);
757
+ }
758
+ }
759
+ debug('search local stream end');
760
+ searchStream.end();
761
+ } catch (err) {
762
+ this.logger.error({ err, query }, 'error on search by plugin @{err.message}');
763
+ searchStream.emit('error', err);
716
764
  }
717
765
  }
766
+ }
718
767
 
719
- debug('uplink list %o', upLinks.length);
768
+ private async removePackageByRevision(pkgName: string, revision: string): Promise<void> {
769
+ const storage: IPackageStorage = this.getPrivatePackageStorage(pkgName);
770
+ debug('get package metadata for %o', pkgName);
771
+ if (typeof storage === 'undefined') {
772
+ throw errorUtils.getServiceUnavailable('storage not initialized');
773
+ }
774
+ let manifest;
775
+ try {
776
+ manifest = await storage.readPackage(pkgName);
777
+ manifest = normalizePackage(manifest);
778
+ } catch (err: any) {
779
+ if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
780
+ logger.info({ pkgName, revision }, 'package not found');
781
+ throw errorUtils.getNotFound();
782
+ }
783
+ logger.error(
784
+ { pkgName, revision, err: err.message },
785
+ 'error @{err} while reading package @{pkgName}-{revision}'
786
+ );
787
+ throw err;
788
+ }
789
+
790
+ // TODO: move this to another method
791
+ try {
792
+ await this.localStorage.getStoragePlugin().remove(pkgName);
793
+ // remove each attachment
794
+ const attachments = Object.keys(manifest._attachments);
795
+ debug('attachments to remove %s', attachments?.length);
796
+ for (let attachment of attachments) {
797
+ debug('remove attachment %s', attachment);
798
+ await storage.deletePackage(attachment);
799
+ logger.info({ attachment }, 'attachment @{attachment} removed');
800
+ }
801
+ // remove package.json
802
+ debug('remove package.json');
803
+ await storage.deletePackage(STORAGE.PACKAGE_FILE_NAME);
804
+ // remove folder
805
+ debug('remove package folder');
806
+ await storage.removePackage();
807
+ logger.info({ pkgName }, 'package @{pkgName} removed');
808
+ } catch (err: any) {
809
+ this.logger.error({ err }, 'removed package has failed @{err.message}');
810
+ throw errorUtils.getBadData(err.message);
811
+ }
812
+ }
720
813
 
721
- async.map(
722
- upLinks,
723
- (upLink, cb): void => {
724
- const _options = Object.assign({}, options);
725
- const upLinkMeta = packageInfo._uplinks[upLink.upname];
814
+ /**
815
+ * Get a package local manifest.
816
+ *
817
+ * Fails if package is not found.
818
+ * @param name package name
819
+ * @param revision of package
820
+ * @returns local manifest
821
+ */
822
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
823
+ public async getPackageLocalMetadata(name: string, _revision?: string): Promise<Manifest> {
824
+ const storage: IPackageStorage = this.getPrivatePackageStorage(name);
825
+ debug('get package metadata for %o', name);
826
+ if (typeof storage === 'undefined') {
827
+ throw errorUtils.getServiceUnavailable('storage not initialized');
828
+ }
726
829
 
727
- if (validatioUtils.isObject(upLinkMeta)) {
728
- const fetched = upLinkMeta.fetched;
830
+ try {
831
+ const result: Manifest = await storage.readPackage(name);
832
+ return normalizePackage(result);
833
+ } catch (err: any) {
834
+ if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
835
+ debug('package %s not found', name);
836
+ throw errorUtils.getNotFound();
837
+ }
838
+ this.logger.error(
839
+ { err: err, file: STORAGE.PACKAGE_FILE_NAME },
840
+ `error reading @{file}: @{!err.message}`
841
+ );
729
842
 
730
- if (fetched && Date.now() - fetched < upLink.maxage) {
731
- return cb();
732
- }
843
+ throw errorUtils.getInternalError();
844
+ }
845
+ }
733
846
 
734
- _options.etag = upLinkMeta.etag;
847
+ /**
848
+ * Fail the stream response with an not found error.
849
+ * @returns
850
+ */
851
+ private createFailureStreamResponseNext(): PassThrough {
852
+ const stream: PassThrough = new PassThrough();
853
+
854
+ // we ensure fails on the next tick into the event loop
855
+ process.nextTick((): void => {
856
+ stream.emit('error', errorUtils.getNotFound(API_ERROR.NO_SUCH_FILE));
857
+ });
858
+
859
+ return stream;
860
+ }
861
+
862
+ /**
863
+ * Update a package and merge tags
864
+ * @param name package name
865
+ * @param tags list of dist-tags
866
+ */
867
+ public async mergeTagsNext(name: string, tags: MergeTags): Promise<Manifest> {
868
+ return await this.updatePackageNext(name, async (data: Manifest): Promise<Manifest> => {
869
+ let newData: Manifest = { ...data };
870
+ for (const tag of Object.keys(tags)) {
871
+ // this handle dist-tag rm command
872
+ if (_.isNull(tags[tag])) {
873
+ delete newData[DIST_TAGS][tag];
874
+ continue;
735
875
  }
736
876
 
737
- upLink.getRemoteMetadata(name, _options, (err, upLinkResponse, eTag): void => {
738
- if (err && err.remoteStatus === 304) {
739
- upLinkMeta.fetched = Date.now();
740
- }
877
+ if (_.isNil(newData.versions[tags[tag]])) {
878
+ throw errorUtils.getNotFound(API_ERROR.VERSION_NOT_EXIST);
879
+ }
880
+ const version: string = tags[tag];
881
+ newData = tagVersionNext(newData, version, tag);
882
+ }
741
883
 
742
- if (err || !upLinkResponse) {
743
- return cb(null, [err || errorUtils.getInternalError('no data')]);
744
- }
884
+ return newData;
885
+ });
886
+ }
745
887
 
746
- try {
747
- validatioUtils.validateMetadata(upLinkResponse, name);
748
- } catch (err: any) {
749
- self.logger.error(
750
- {
751
- sub: 'out',
752
- err: err,
753
- },
754
- 'package.json validating error @{!err?.message}\n@{err.stack}'
755
- );
756
- return cb(null, [err]);
757
- }
888
+ private getUpLinkForDistFile(pkgName: string, distFile: DistFile): IProxy {
889
+ let uplink: IProxy | null = null;
758
890
 
759
- packageInfo._uplinks[upLink.upname] = {
760
- etag: eTag,
761
- fetched: Date.now(),
762
- };
891
+ for (const uplinkId in this.uplinks) {
892
+ // refer to https://github.com/verdaccio/verdaccio/issues/1642
893
+ if (hasProxyTo(pkgName, uplinkId, this.config.packages)) {
894
+ uplink = this.uplinks[uplinkId];
895
+ }
896
+ }
763
897
 
764
- packageInfo.time = mergeUplinkTimeIntoLocal(packageInfo, upLinkResponse);
898
+ if (uplink == null) {
899
+ debug('upstream not found creating one for %o', pkgName);
900
+ uplink = new ProxyStorage(
901
+ {
902
+ url: distFile.url,
903
+ cache: true,
904
+ },
905
+ this.config
906
+ );
907
+ }
908
+ return uplink;
909
+ }
765
910
 
766
- updateVersionsHiddenUpLink(upLinkResponse.versions, upLink);
911
+ public async updateLocalMetadata(pkgName: string) {
912
+ const storage = this.getPrivatePackageStorage(pkgName);
767
913
 
768
- try {
769
- pkgUtils.mergeVersions(packageInfo, upLinkResponse);
770
- } catch (err: any) {
771
- self.logger.error(
772
- {
773
- sub: 'out',
774
- err: err,
775
- },
776
- 'package.json parsing error @{!err?.message}\n@{err.stack}'
777
- );
778
- return cb(null, [err]);
779
- }
914
+ if (!storage) {
915
+ throw errorUtils.getNotFound();
916
+ }
917
+ }
780
918
 
781
- // if we got to this point, assume that the correct package exists
782
- // on the uplink
783
- found = true;
784
- cb();
919
+ public async updateManifest(manifest: Manifest, options: UpdateManifestOptions): Promise<void> {
920
+ if (isDeprecatedManifest(manifest)) {
921
+ // if the manifest is deprecated, we need to update the package.json
922
+ await this.deprecate(manifest, {
923
+ ...options,
924
+ });
925
+ } else if (
926
+ isPublishablePackage(manifest) === false &&
927
+ validatioUtils.isObject(manifest.users)
928
+ ) {
929
+ // if user request to apply a star to the manifest
930
+ await this.star(manifest, {
931
+ ...options,
932
+ });
933
+ } else if (validatioUtils.validatePublishSingleVersion(manifest)) {
934
+ // if continue, the version to be published does not exist
935
+ // we create a new package
936
+ const [mergedManifest, version] = await this.publishANewVersion(manifest, {
937
+ ...options,
938
+ });
939
+ // send notification of publication (notification step, non transactional)
940
+ try {
941
+ const { name } = mergedManifest;
942
+ await this.notify(mergedManifest, `${name}@${version}`);
943
+ logger.info({ name, version }, 'notify for @{name}@@{version} has been sent');
944
+ } catch (error: any) {
945
+ logger.error({ error: error.message }, 'notify batch service has failed: @{error}');
946
+ }
947
+ } else {
948
+ debug('invalid body format');
949
+ logger.info(
950
+ { packageName: options.name },
951
+ `wrong package format on publish a package @{packageName}`
952
+ );
953
+ throw errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL);
954
+ }
955
+ }
956
+
957
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
958
+ private async deprecate(_body: Manifest, _options: PublishOptions): Promise<void> {
959
+ // // const storage: IPackageStorage = this.getPrivatePackageStorage(opname);
960
+
961
+ // if (typeof storage === 'undefined') {
962
+ // throw errorUtils.getNotFound();
963
+ // }
964
+ throw errorUtils.getInternalError('no implemenation ready for npm deprecate');
965
+ }
966
+
967
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
968
+ private async star(_body: Manifest, _options: PublishOptions): Promise<void> {
969
+ // // const storage: IPackageStorage = this.getPrivatePackageStorage(opname);
970
+
971
+ // if (typeof storage === 'undefined') {
972
+ // throw errorUtils.getNotFound();
973
+ // }
974
+
975
+ throw errorUtils.getInternalError('no implemenation ready for npm star');
976
+ }
977
+
978
+ /**
979
+ * Get local package, on fails return null.
980
+ * Errors are considered package not found.
981
+ * @param name
982
+ * @returns
983
+ */
984
+ private async getPackagelocalByNameNext(name: string): Promise<Manifest | null> {
985
+ try {
986
+ return await this.getPackageLocalMetadata(name);
987
+ } catch (err: any) {
988
+ debug('local package %s not found', name);
989
+ return null;
990
+ }
991
+ }
992
+
993
+ /**
994
+ * Convert tarball as string into a Buffer and validate the length.
995
+ * @param data the tarball data as string
996
+ * @returns
997
+ */
998
+ private getBufferManifest(data: string): Buffer {
999
+ const buffer = Buffer.from(data, 'base64');
1000
+ if (buffer.length === 0) {
1001
+ throw errorUtils.getBadData('refusing to accept zero-length file');
1002
+ }
1003
+ return buffer;
1004
+ }
1005
+
1006
+ /**
1007
+ * Verify if the package exists in the local storage
1008
+ * (the package refers to the package.json), directory would return false.
1009
+ * @param pkgName package name
1010
+ * @returns boolean
1011
+ */
1012
+ private async hasPackage(pkgName: string): Promise<boolean> {
1013
+ const storage: IPackageStorage = this.getPrivatePackageStorage(pkgName);
1014
+ if (typeof storage === 'undefined') {
1015
+ throw errorUtils.getNotFound();
1016
+ }
1017
+ const hasPackage = await storage.hasPackage();
1018
+ debug('has package %o for %o', pkgName, hasPackage);
1019
+ return hasPackage;
1020
+ }
1021
+
1022
+ /**
1023
+ * Create a new package.
1024
+ * This situation happens only of the package does not exist on the cache.
1025
+ *
1026
+ * @param body package metadata
1027
+ * @param options
1028
+ * @returns
1029
+ */
1030
+ private async publishANewVersion(
1031
+ body: Manifest,
1032
+ options: PublishOptions
1033
+ ): Promise<[Manifest, string]> {
1034
+ const { name } = options;
1035
+ debug('publishing a new package for %o', name);
1036
+
1037
+ const manifest: Manifest = { ...validatioUtils.normalizeMetadata(body, name) };
1038
+ const { _attachments, versions } = manifest;
1039
+
1040
+ // validation step, if _attachments is not an object throw error
1041
+ // _attachments is need it for holding the tarball buffer in the local storage as file
1042
+ // versions is need it for holding the version in the local storage as file
1043
+ // _attachments and validation are required otherwise cannot continue.
1044
+ if (isEmpty(_attachments)) {
1045
+ throw errorUtils.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL);
1046
+ }
1047
+
1048
+ // get the unique version available
1049
+ const [versionToPublish] = Object.keys(versions);
1050
+
1051
+ // at this point document is either created or existed before
1052
+ const [firstAttachmentKey] = Object.keys(_attachments);
1053
+ const buffer = this.getBufferManifest(body._attachments[firstAttachmentKey].data as string);
1054
+
1055
+ try {
1056
+ // we check if package exist already locally
1057
+ const manifest = await this.getPackagelocalByNameNext(name);
1058
+ // if continue, the version to be published does not exist
1059
+ if (manifest?.versions[versionToPublish] != null) {
1060
+ debug('%s version %s already exists', name, versionToPublish);
1061
+ throw errorUtils.getConflict();
1062
+ }
1063
+
1064
+ // if execution get here, package does not exist locally, we search upstream
1065
+ const remoteManifest = await this.checkPackageRemote(name, this.isAllowPublishOffline());
1066
+ if (remoteManifest?.versions[versionToPublish] != null) {
1067
+ debug('%s version %s already exists', name, versionToPublish);
1068
+ throw errorUtils.getConflict();
1069
+ }
1070
+
1071
+ const hasPackageInStorage = await this.hasPackage(name);
1072
+ if (!hasPackageInStorage) {
1073
+ await this.createNewLocalCachePackage(name, versionToPublish);
1074
+ }
1075
+ } catch (err: any) {
1076
+ debug('error on change or update a package with %o', err.message);
1077
+ logger.error({ err: err.message }, 'error on create package: @{err}');
1078
+ throw err;
1079
+ }
1080
+
1081
+ // 1. after tarball has been successfully uploaded, we update the version
1082
+ try {
1083
+ // TODO: review why do this
1084
+ versions[versionToPublish].readme =
1085
+ _.isNil(manifest.readme) === false ? String(manifest.readme) : '';
1086
+ await this.addVersionNext(name, versionToPublish, versions[versionToPublish], null);
1087
+ } catch (err: any) {
1088
+ logger.error({ err: err.message }, 'updated version has failed: @{err}');
1089
+ debug('error on create a version for %o with error %o', name, err.message);
1090
+ // TODO: remove tarball if add version fails
1091
+ throw err;
1092
+ }
1093
+
1094
+ // 2. update and merge tags
1095
+ let mergedManifest;
1096
+ try {
1097
+ // note: I could merge this with addVersionNext
1098
+ // 1. add version
1099
+ // 2. merge versions
1100
+ // 3. upload tarball
1101
+ // 3.update once to the storage (easy peasy)
1102
+ mergedManifest = await this.mergeTagsNext(name, manifest[DIST_TAGS]);
1103
+ } catch (err: any) {
1104
+ logger.error({ err: err.message }, 'merge version has failed: @{err}');
1105
+ debug('error on create a version for %o with error %o', name, err.message);
1106
+ // TODO: undo if this fails
1107
+ // 1. remove tarball
1108
+ // 2. remove updated version
1109
+ throw err;
1110
+ }
1111
+
1112
+ // 3. upload the tarball to the storage
1113
+ try {
1114
+ const readable = Readable.from(buffer);
1115
+ await this.uploadTarball(name, firstAttachmentKey, readable, {
1116
+ signal: options.signal,
1117
+ });
1118
+ } catch (err: any) {
1119
+ logger.error({ err: err.message }, 'upload tarball has failed: @{err}');
1120
+ throw err;
1121
+ }
1122
+
1123
+ logger.info(
1124
+ { name, version: versionToPublish },
1125
+ 'package @{name}@@{version} has been published'
1126
+ );
1127
+
1128
+ return [mergedManifest, versionToPublish];
1129
+ }
1130
+
1131
+ // TODO: pending implementation
1132
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1133
+ private async notify(_manifest: Manifest, _message: string): Promise<void> {
1134
+ return;
1135
+ }
1136
+
1137
+ private getProxyList() {
1138
+ const uplinksList = Object.keys(this.uplinks);
1139
+
1140
+ return uplinksList;
1141
+ }
1142
+
1143
+ /**
1144
+ * Wrap uploadTarballAsStream into a promise.
1145
+ * @param name package name
1146
+ * @param fileName tarball name
1147
+ * @param contentReadable content as readable stream
1148
+ * @param options
1149
+ * @returns
1150
+ */
1151
+ public async uploadTarball(
1152
+ name: string,
1153
+ fileName: string,
1154
+ contentReadable: Readable,
1155
+ { signal }
1156
+ ): Promise<void> {
1157
+ return new Promise((resolve, reject) => {
1158
+ (async () => {
1159
+ const stream: Writable = await this.uploadTarballAsStream(name, fileName, {
1160
+ signal,
1161
+ });
1162
+
1163
+ stream.on('error', (err) => {
1164
+ debug(
1165
+ 'error on stream a tarball %o for %o with error %o',
1166
+ 'foo.tar.gz',
1167
+ name,
1168
+ err.message
1169
+ );
1170
+ reject(err);
1171
+ });
1172
+ stream.on('success', () => {
1173
+ this.logger.debug(
1174
+ { fileName, name },
1175
+ 'file @{fileName} for package @{name} has been succesfully uploaded'
1176
+ );
1177
+ resolve();
785
1178
  });
1179
+
1180
+ await pipeline(contentReadable, stream, { signal });
1181
+ })().catch((err) => {
1182
+ reject(err);
1183
+ });
1184
+ });
1185
+ }
1186
+
1187
+ public async uploadTarballAsStream(
1188
+ pkgName: string,
1189
+ filename: string,
1190
+ { signal }
1191
+ ): Promise<PassThrough> {
1192
+ debug(`add a tarball for %o`, pkgName);
1193
+ assert(validatioUtils.validateName(filename));
1194
+
1195
+ const shaOneHash = createTarballHash();
1196
+ const transformHash = new Transform({
1197
+ transform(chunk: any, _encoding: string, callback: any): void {
1198
+ // measure the length for validation reasons
1199
+ shaOneHash.update(chunk);
1200
+ callback(null, chunk);
786
1201
  },
787
- // @ts-ignore
788
- (err: Error, upLinksErrors: any): AsyncResultArrayCallback<unknown, Error> => {
789
- assert(!err && Array.isArray(upLinksErrors));
790
-
791
- // Check for connection timeout or reset errors with uplink(s)
792
- // (these should be handled differently from the package not being found)
793
- if (!found) {
794
- let uplinkTimeoutError;
795
- for (let i = 0; i < upLinksErrors.length; i++) {
796
- if (upLinksErrors[i]) {
797
- for (let j = 0; j < upLinksErrors[i].length; j++) {
798
- if (upLinksErrors[i][j]) {
799
- const code = upLinksErrors[i][j].code;
800
- if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT' || code === 'ECONNRESET') {
801
- uplinkTimeoutError = true;
802
- break;
803
- }
804
- }
805
- }
1202
+ });
1203
+ const uploadStream = new PassThrough();
1204
+ const storage = this.getPrivatePackageStorage(pkgName);
1205
+
1206
+ // FUTURE: this validation could happen even before
1207
+ if (pkgName === PROTO_NAME) {
1208
+ process.nextTick((): void => {
1209
+ uploadStream.emit('error', errorUtils.getForbidden());
1210
+ });
1211
+ return uploadStream;
1212
+ }
1213
+
1214
+ // FIXME: this condition will never met, storage is always defined
1215
+ if (!storage) {
1216
+ process.nextTick((): void => {
1217
+ uploadStream.emit('error', "can't upload this package storage is missing");
1218
+ });
1219
+ return uploadStream;
1220
+ }
1221
+
1222
+ const fileDoesExist = await storage.hasTarball(filename);
1223
+ if (fileDoesExist) {
1224
+ process.nextTick((): void => {
1225
+ uploadStream.emit('error', errorUtils.getConflict());
1226
+ });
1227
+ } else {
1228
+ const localStorageWriteStream = await storage.writeTarball(filename, { signal });
1229
+
1230
+ localStorageWriteStream.on('open', async () => {
1231
+ await pipeline(uploadStream, transformHash, localStorageWriteStream, { signal });
1232
+ });
1233
+
1234
+ // once the file descriptor has been closed
1235
+ localStorageWriteStream.on('close', async () => {
1236
+ try {
1237
+ debug('uploaded tarball %o for %o', filename, pkgName);
1238
+ // update the package metadata
1239
+ await this.updatePackageNext(pkgName, async (data: Manifest): Promise<Manifest> => {
1240
+ const newData: Manifest = { ...data };
1241
+ debug('added _attachment for %o', pkgName);
1242
+ newData._attachments[filename] = {
1243
+ // TODO: add integrity hash here
1244
+ shasum: shaOneHash.digest('hex'),
1245
+ };
1246
+
1247
+ return newData;
1248
+ });
1249
+ debug('emit success for %o', pkgName);
1250
+ uploadStream.emit('success');
1251
+ } catch (err: any) {
1252
+ // FUTURE: if the update package fails, remove tarball to avoid left
1253
+ // orphan tarballs
1254
+ debug(
1255
+ 'something has failed on upload tarball %o for %o : %s',
1256
+ filename,
1257
+ pkgName,
1258
+ err.message
1259
+ );
1260
+ uploadStream.emit('error', err);
1261
+ }
1262
+ });
1263
+
1264
+ // something went wrong writing into the local storage
1265
+ localStorageWriteStream.on('error', async (err: any) => {
1266
+ uploadStream.emit('error', err);
1267
+ });
1268
+ }
1269
+
1270
+ return uploadStream;
1271
+ }
1272
+
1273
+ /**
1274
+ * Add a new version to a package.
1275
+ * @param name package name
1276
+ * @param version version
1277
+ * @param metadata version metadata
1278
+ * @param tag tag of the version
1279
+ */
1280
+ public async addVersionNext(
1281
+ name: string,
1282
+ version: string,
1283
+ metadata: Version,
1284
+ tag: StringValue
1285
+ ): Promise<void> {
1286
+ debug(`add version %s package for %s`, version, name);
1287
+ await this.updatePackageNext(name, async (data: Manifest): Promise<Manifest> => {
1288
+ // keep only one readme per package
1289
+ data.readme = metadata.readme;
1290
+ debug('%s` readme mutated', name);
1291
+ // TODO: lodash remove
1292
+ metadata = cleanUpReadme(metadata);
1293
+ metadata.contributors = normalizeContributors(metadata.contributors as Author[]);
1294
+ debug('%s` contributors normalized', name);
1295
+
1296
+ // if uploaded tarball has a different shasum, it's very likely that we
1297
+ // have some kind of error
1298
+ if (validatioUtils.isObject(metadata.dist) && _.isString(metadata.dist.tarball)) {
1299
+ const tarball = metadata.dist.tarball.replace(/.*\//, '');
1300
+ if (validatioUtils.isObject(data._attachments[tarball])) {
1301
+ if (
1302
+ _.isNil(data._attachments[tarball].shasum) === false &&
1303
+ _.isNil(metadata.dist.shasum) === false
1304
+ ) {
1305
+ if (data._attachments[tarball].shasum != metadata.dist.shasum) {
1306
+ const errorMessage =
1307
+ `shasum error, ` +
1308
+ `${data._attachments[tarball].shasum} != ${metadata.dist.shasum}`;
1309
+ throw errorUtils.getBadRequest(errorMessage);
806
1310
  }
807
1311
  }
1312
+ data._attachments[tarball].version = version;
1313
+ }
808
1314
 
809
- if (uplinkTimeoutError) {
810
- return callback(errorUtils.getServiceUnavailable(), null, upLinksErrors);
811
- }
812
- return callback(errorUtils.getNotFound(API_ERROR.NO_PACKAGE), null, upLinksErrors);
1315
+ // if the time field doesn't exist, we create it, some old storage
1316
+ // might not have it
1317
+ // https://github.com/verdaccio/verdaccio/issues/740
1318
+ if (_.isNil(data.time)) {
1319
+ this.logger.warn(
1320
+ { name },
1321
+ 'time field could not be found, it has been recreated for @{name}'
1322
+ );
1323
+ data.time = {};
813
1324
  }
814
1325
 
815
- if (upLinks.length === 0) {
816
- return callback(null, packageInfo);
1326
+ // populate the time field with the date of the version
1327
+ const currentDate = new Date().toISOString();
1328
+ data.time.modified = currentDate;
1329
+ if (!data.time?.created) {
1330
+ data.time.created = currentDate;
817
1331
  }
818
1332
 
819
- self.localStorage.updateVersions(
820
- name,
821
- packageInfo,
822
- async (err, packageJsonLocal: Package): Promise<any> => {
823
- if (err) {
824
- return callback(err);
825
- }
826
- // Any error here will cause a 404, like an uplink error. This is likely
827
- // the right thing to do
828
- // as a broken filter is a security risk.
829
- const filterErrors: Error[] = [];
830
- // This MUST be done serially and not in parallel as they modify packageJsonLocal
831
- for (const filter of self.filters) {
832
- try {
833
- // These filters can assume it's save to modify packageJsonLocal
834
- // and return it directly for
835
- // performance (i.e. need not be pure)
836
- packageJsonLocal = await filter.filter_metadata(packageJsonLocal);
837
- } catch (err: any) {
838
- filterErrors.push(err);
839
- }
1333
+ if (typeof data.time[version] === 'string') {
1334
+ // this should not h appen, but we keep this check to avoid or easy bug report
1335
+ this.logger.warn(
1336
+ { name, version },
1337
+ 'the time for the version @{version} already exists, it has been overwritten for package @{name}'
1338
+ );
1339
+ }
1340
+ data.time[version] = currentDate;
1341
+ debug('time added for %s version %s', name, version);
1342
+ }
1343
+
1344
+ data.versions[version] = metadata;
1345
+ // TODO: review this method, it's a bit ugly
1346
+ tagVersion(data, version, tag);
1347
+
1348
+ try {
1349
+ debug('%s` add on database', name);
1350
+ await this.localStorage.getStoragePlugin().add(name);
1351
+ this.logger.debug({ name, version }, 'version @{version} added to database for @{name}');
1352
+ } catch (err: any) {
1353
+ throw errorUtils.getBadData(err.message);
1354
+ }
1355
+ return data;
1356
+ });
1357
+ }
1358
+
1359
+ /**
1360
+ * Create an empty new local cache package without versions.
1361
+ * @param name name of the package
1362
+ * @returns
1363
+ */
1364
+ private async createNewLocalCachePackage(name: string, latestVersion: string): Promise<void> {
1365
+ const storage: IPackageStorage = this.getPrivatePackageStorage(name);
1366
+
1367
+ if (!storage) {
1368
+ debug(`storage is missing for %o package cannot be added`, name);
1369
+ throw errorUtils.getNotFound('this package cannot be added');
1370
+ }
1371
+
1372
+ const currentTime = new Date().toISOString();
1373
+ const packageData: Manifest = {
1374
+ ...generatePackageTemplate(name),
1375
+ time: {
1376
+ created: currentTime,
1377
+ modified: currentTime,
1378
+ [latestVersion]: currentTime,
1379
+ },
1380
+ };
1381
+
1382
+ try {
1383
+ await storage.createPackage(name, packageData);
1384
+ this.logger.info({ name }, 'created new package @{name}');
1385
+ return;
1386
+ } catch (err: any) {
1387
+ if (
1388
+ _.isNull(err) === false &&
1389
+ (err.code === STORAGE.FILE_EXIST_ERROR || err.code === HTTP_STATUS.CONFLICT)
1390
+ ) {
1391
+ debug(`error on creating a package for %o with error %o`, name, err.message);
1392
+ throw errorUtils.getConflict();
1393
+ }
1394
+ return;
1395
+ }
1396
+ }
1397
+
1398
+ private isAllowPublishOffline(): boolean {
1399
+ return (
1400
+ typeof this.config.publish !== 'undefined' &&
1401
+ _.isBoolean(this.config.publish.allow_offline) &&
1402
+ this.config.publish.allow_offline
1403
+ );
1404
+ }
1405
+
1406
+ /**
1407
+ *
1408
+ * @param name package name
1409
+ * @param uplinksLook
1410
+ * @returns
1411
+ */
1412
+ private async checkPackageRemote(name: string, uplinksLook: boolean): Promise<Manifest | null> {
1413
+ try {
1414
+ // we provide a null manifest, thus the manifest returned will be the remote one
1415
+ const [remoteManifest, upLinksErrors] = await this.syncUplinksMetadataNext(name, null, {
1416
+ uplinksLook,
1417
+ });
1418
+
1419
+ // checking package exist already
1420
+ if (isNil(remoteManifest) === false) {
1421
+ throw errorUtils.getConflict(API_ERROR.PACKAGE_EXIST);
1422
+ }
1423
+
1424
+ for (let errorItem = 0; errorItem < upLinksErrors.length; errorItem++) {
1425
+ // checking error
1426
+ // if uplink fails with a status other than 404, we report failure
1427
+ if (isNil(upLinksErrors[errorItem][0]) === false) {
1428
+ if (upLinksErrors[errorItem][0].status !== HTTP_STATUS.NOT_FOUND) {
1429
+ if (upLinksErrors) {
1430
+ return null;
840
1431
  }
841
- callback(null, packageJsonLocal, _.concat(upLinksErrors, filterErrors));
1432
+
1433
+ throw errorUtils.getServiceUnavailable(API_ERROR.UPLINK_OFFLINE_PUBLISH);
842
1434
  }
1435
+ }
1436
+ }
1437
+ return remoteManifest;
1438
+ } catch (err: any) {
1439
+ if (err && err.status !== HTTP_STATUS.NOT_FOUND) {
1440
+ throw err;
1441
+ }
1442
+ return null;
1443
+ }
1444
+ }
1445
+
1446
+ private setDefaultRevision(json: Manifest): Manifest {
1447
+ // calculate revision from couch db
1448
+ if (_.isString(json._rev) === false) {
1449
+ json._rev = STORAGE.DEFAULT_REVISION;
1450
+ }
1451
+
1452
+ // this is intended in debug mode we do not want modify the store revision
1453
+ if (_.isNil(this.config._debug)) {
1454
+ json._rev = generateRevision(json._rev);
1455
+ }
1456
+
1457
+ return json;
1458
+ }
1459
+
1460
+ private async writePackageNext(name: string, json: Manifest): Promise<void> {
1461
+ const storage: any = this.getPrivatePackageStorage(name);
1462
+ if (_.isNil(storage)) {
1463
+ // TODO: replace here 500 error
1464
+ throw errorUtils.getBadData();
1465
+ }
1466
+ await storage.savePackage(name, this.setDefaultRevision(json));
1467
+ }
1468
+
1469
+ /**
1470
+ * @param {*} name package name
1471
+ * @param {*} updateHandler function(package, cb) - update function
1472
+ * @param {*} callback callback that gets invoked after it's all updated
1473
+ * @return {Function}
1474
+ */
1475
+ private async updatePackageNext(
1476
+ name: string,
1477
+ updateHandler: (manifest: Manifest) => Promise<Manifest>
1478
+ ): Promise<Manifest> {
1479
+ const storage: IPackageStorage = this.getPrivatePackageStorage(name);
1480
+
1481
+ if (!storage) {
1482
+ throw errorUtils.getNotFound();
1483
+ }
1484
+
1485
+ // we update the package on the local storage
1486
+ const updatedManifest: Manifest = await storage.updatePackage(name, updateHandler);
1487
+ // after correctly updated write to the storage
1488
+ try {
1489
+ await this.writePackageNext(name, normalizePackage(updatedManifest));
1490
+ return updatedManifest;
1491
+ } catch (err: any) {
1492
+ if (err.code === resourceNotAvailable) {
1493
+ throw errorUtils.getInternalError('resource temporarily unavailable');
1494
+ } else if (err.code === noSuchFile) {
1495
+ throw errorUtils.getNotFound();
1496
+ } else {
1497
+ throw err;
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ *
1504
+ * @protected
1505
+ * @param {IGetPackageOptionsNext} options
1506
+ * @return {*} {Promise<[Manifest, any[]]>}
1507
+ * @memberof AbstractStorage
1508
+ */
1509
+ private async getPackageNext(options: IGetPackageOptionsNext): Promise<[Manifest, any[]]> {
1510
+ const { name } = options;
1511
+ debug('get package for %o', name);
1512
+ let data: Manifest | null = null;
1513
+
1514
+ try {
1515
+ data = await this.getPackageLocalMetadata(name);
1516
+ } catch (err: any) {
1517
+ // if error code is higher than 500 stop here
1518
+ if (err && (!err.status || err.status >= HTTP_STATUS.INTERNAL_ERROR)) {
1519
+ throw err;
1520
+ }
1521
+ }
1522
+
1523
+ // if we can't get the local metadata, we try to get the remote metadata
1524
+ // if we do to have local metadata, we try to update it with the upstream registry
1525
+ debug('sync uplinks for %o', name);
1526
+ const [remoteManifest, upLinksErrors] = await this.syncUplinksMetadataNext(name, data, {
1527
+ uplinksLook: options.uplinksLook,
1528
+ retry: options.retry,
1529
+ remoteAddress: options.requestOptions.remoteAddress,
1530
+ // etag??
1531
+ });
1532
+
1533
+ // if either local data and upstream data are empty, we throw an error
1534
+ if (!remoteManifest && _.isNull(data)) {
1535
+ throw errorUtils.getNotFound(`${API_ERROR.NOT_PACKAGE_UPLINK}: ${name}`);
1536
+ // if the remote manifest is empty, we return local data
1537
+ } else if (!remoteManifest && !_.isNull(data)) {
1538
+ // no data on uplinks
1539
+ return [data as Manifest, upLinksErrors];
1540
+ }
1541
+
1542
+ // if we have local data, we try to update it with the upstream registry
1543
+ const normalizedPkg = Object.assign({}, remoteManifest, {
1544
+ // FIXME: clean up mutation within cleanUpLinksRef method
1545
+ ...normalizeDistTags(cleanUpLinksRef(remoteManifest as Manifest, options.keepUpLinkData)),
1546
+ _attachments: {},
1547
+ });
1548
+
1549
+ debug('no. sync uplinks errors %o for %s', upLinksErrors?.length, name);
1550
+ return [normalizedPkg, upLinksErrors];
1551
+ }
1552
+
1553
+ /**
1554
+ * Function fetches package metadata from uplinks and synchronizes it with local data
1555
+ if package is available locally, it MUST be provided in pkginfo.
1556
+
1557
+ Using this example:
1558
+
1559
+ "jquery":
1560
+ access: $all
1561
+ publish: $authenticated
1562
+ unpublish: $authenticated
1563
+ # two uplinks setup
1564
+ proxy: ver npmjs
1565
+ # one uplink setup
1566
+ proxy: npmjs
1567
+
1568
+ A package requires uplinks syncronization if enables the proxy section, uplinks
1569
+ can be more than one, the more are the most slow request will take, the request
1570
+ are made in serie and if 1st call fails, the second will be triggered, otherwise
1571
+ the 1st will reply and others will be discareded. The order is important.
1572
+
1573
+ Errors on upkinks are considered are, time outs, connection fails and http status 304,
1574
+ in that case the request returns empty body and we want ask next on the list if has fresh
1575
+ updates.
1576
+ */
1577
+ public async syncUplinksMetadataNext(
1578
+ name: string,
1579
+ localManifest: Manifest | null,
1580
+ options: ISyncUplinksOptions = {}
1581
+ ): Promise<[Manifest | null, any]> {
1582
+ let found = localManifest !== null;
1583
+ let syncManifest: Manifest | null = null;
1584
+ const upLinks: string[] = [];
1585
+ const hasToLookIntoUplinks = _.isNil(options.uplinksLook) || options.uplinksLook;
1586
+ debug('is sync uplink enabled %o', hasToLookIntoUplinks);
1587
+
1588
+ for (const uplink in this.uplinks) {
1589
+ if (hasProxyTo(name, uplink, this.config.packages) && hasToLookIntoUplinks) {
1590
+ debug('sync uplink %o', uplink);
1591
+ upLinks.push(uplink);
1592
+ }
1593
+ }
1594
+
1595
+ // if none uplink match we return the local manifest
1596
+ if (upLinks.length === 0) {
1597
+ debug('no uplinks found for %o upstream update aborted', name);
1598
+ return [localManifest, []];
1599
+ }
1600
+
1601
+ const uplinksErrors: any[] = [];
1602
+ // we resolve uplinks async in serie, first come first serve
1603
+ for (const uplink of upLinks) {
1604
+ try {
1605
+ const tempManifest = _.isNil(localManifest)
1606
+ ? generatePackageTemplate(name)
1607
+ : { ...localManifest };
1608
+ syncManifest = await this.mergeCacheRemoteMetadata(
1609
+ this.uplinks[uplink],
1610
+ tempManifest,
1611
+ options
843
1612
  );
1613
+ debug('syncing on uplink %o', syncManifest.name);
1614
+ if (_.isNil(syncManifest) === false) {
1615
+ found = true;
1616
+ break;
1617
+ }
1618
+ } catch (err: any) {
1619
+ debug('error captured on uplink %o', err.message);
1620
+ uplinksErrors.push(err);
1621
+ // enforce use next uplink on the list
1622
+ continue;
1623
+ }
1624
+ }
1625
+
1626
+ if (found && syncManifest !== null) {
1627
+ // updates the local cache manifest with fresh data
1628
+ let updatedCacheManifest = await this.updateVersionsNext(name, syncManifest);
1629
+ // plugin filter applied to the manifest
1630
+ const [filteredManifest, filtersErrors] = await this.applyFilters(updatedCacheManifest);
1631
+ return [
1632
+ { ...updatedCacheManifest, ...filteredManifest },
1633
+ [...uplinksErrors, ...filtersErrors],
1634
+ ];
1635
+ } else if (found && _.isNil(localManifest) === false) {
1636
+ return [localManifest, uplinksErrors];
1637
+ } else {
1638
+ // if is not found, calculate the right error to return
1639
+ debug('uplinks sync failed with %o errors', uplinksErrors.length);
1640
+ for (const err of uplinksErrors) {
1641
+ const { code } = err;
1642
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT' || code === 'ECONNRESET') {
1643
+ debug('uplinks sync failed with timeout error');
1644
+ throw errorUtils.getServiceUnavailable(err.code);
1645
+ }
1646
+ // we bubble up the 304 special error case
1647
+ if (code === HTTP_STATUS.NOT_MODIFIED) {
1648
+ debug('uplinks sync failed with 304 error');
1649
+ throw err;
1650
+ }
844
1651
  }
1652
+ debug('uplinks sync failed with no package found');
1653
+
1654
+ throw errorUtils.getNotFound(API_ERROR.NO_PACKAGE);
1655
+ }
1656
+ }
1657
+
1658
+ /**
1659
+ * Merge a manifest with a remote manifest.
1660
+ *
1661
+ * If the uplinks are not available, the local manifest is returned.
1662
+ * If the uplinks are available, the local manifest is merged with the remote one.
1663
+ *
1664
+ *
1665
+ * @param uplink uplink instance
1666
+ * @param cachedManifest the local cached manifest
1667
+ * @param options options
1668
+ * @returns Returns a promise that resolves with the merged manifest.
1669
+ */
1670
+ private async mergeCacheRemoteMetadata(
1671
+ uplink: IProxy,
1672
+ cachedManifest: Manifest,
1673
+ options: ISyncUplinksOptions
1674
+ ): Promise<Manifest> {
1675
+ // we store which uplink is updating the manifest
1676
+ const upLinkMeta = cachedManifest._uplinks[uplink.upname];
1677
+ let _cacheManifest = { ...cachedManifest };
1678
+
1679
+ if (validatioUtils.isObject(upLinkMeta)) {
1680
+ const fetched = upLinkMeta.fetched;
1681
+
1682
+ // we check the uplink cache is fresh
1683
+ if (fetched && Date.now() - fetched < uplink.maxage) {
1684
+ return cachedManifest;
1685
+ }
1686
+ }
1687
+
1688
+ const remoteOptions = Object.assign({}, options, {
1689
+ etag: upLinkMeta?.etag,
1690
+ });
1691
+
1692
+ // get the latest metadata from the uplink
1693
+ const [remoteManifest, etag] = await uplink.getRemoteMetadataNext(
1694
+ _cacheManifest.name,
1695
+ remoteOptions
845
1696
  );
1697
+
1698
+ try {
1699
+ _cacheManifest = validatioUtils.normalizeMetadata(_cacheManifest, _cacheManifest.name);
1700
+ } catch (err: any) {
1701
+ this.logger.error(
1702
+ {
1703
+ err: err,
1704
+ },
1705
+ 'package.json validating error @{!err?.message}\n@{err.stack}'
1706
+ );
1707
+ throw err;
1708
+ }
1709
+ // updates the _uplink metadata fields, cache, etc
1710
+ _cacheManifest = updateUpLinkMetadata(uplink.upname, _cacheManifest, etag);
1711
+ // merge time field cache and remote
1712
+ _cacheManifest = mergeUplinkTimeIntoLocalNext(_cacheManifest, remoteManifest);
1713
+ // update the _uplinks field in the cache
1714
+ _cacheManifest = updateVersionsHiddenUpLinkNext(_cacheManifest, uplink);
1715
+ try {
1716
+ // merge versions from remote into the cache
1717
+ _cacheManifest = mergeVersions(_cacheManifest, remoteManifest);
1718
+ return _cacheManifest;
1719
+ } catch (err: any) {
1720
+ this.logger.error(
1721
+ {
1722
+ err: err,
1723
+ },
1724
+ 'package.json merge has failed @{!err?.message}\n@{err.stack}'
1725
+ );
1726
+ throw err;
1727
+ }
846
1728
  }
847
1729
 
848
1730
  /**
849
- * Set a hidden value for each version.
850
- * @param {Array} versions list of version
851
- * @param {String} upLink uplink name
1731
+ * Apply filters to manifest.
1732
+ * @param manifest
1733
+ * @returns
1734
+ */
1735
+ public async applyFilters(manifest: Manifest): Promise<[Manifest, any]> {
1736
+ if (this.filters.length === 0) {
1737
+ return [manifest, []];
1738
+ }
1739
+
1740
+ let filterPluginErrors: any[] = [];
1741
+ let filteredManifest = { ...manifest };
1742
+ for (const filter of this.filters) {
1743
+ // These filters can assume it's save to modify packageJsonLocal
1744
+ // and return it directly for
1745
+ // performance (i.e. need not be pure)
1746
+ try {
1747
+ filteredManifest = await filter.filter_metadata(manifest);
1748
+ } catch (err: any) {
1749
+ this.logger.error({ err: err.message }, 'filter has failed @{err}');
1750
+ filterPluginErrors.push(err);
1751
+ }
1752
+ }
1753
+ return [filteredManifest, filterPluginErrors];
1754
+ }
1755
+
1756
+ private _createNewPackageNext(name: string): Manifest {
1757
+ return normalizePackage(generatePackageTemplate(name));
1758
+ }
1759
+
1760
+ /**
1761
+ * Ensure the dist file remains as the same protocol
1762
+ * @param {Object} hash metadata
1763
+ * @param {String} upLinkKey registry key
852
1764
  * @private
1765
+ * @deprecated use _updateUplinkToRemoteProtocolNext (???)
853
1766
  */
854
- public _updateVersionsHiddenUpLink(versions: Versions, upLink: IProxy): void {
855
- for (const i in versions) {
856
- if (Object.prototype.hasOwnProperty.call(versions, i)) {
857
- const version = versions[i];
1767
+ private updateUplinkToRemoteProtocol(hash: DistFile, upLinkKey: string): void {
1768
+ // if we got this information from a known registry,
1769
+ // use the same protocol for the tarball
1770
+ const tarballUrl: any = URL.parse(hash.url);
1771
+ const uplinkUrl: any = URL.parse(this.config.uplinks[upLinkKey].url);
1772
+
1773
+ if (uplinkUrl.host === tarballUrl.host) {
1774
+ tarballUrl.protocol = uplinkUrl.protocol;
1775
+ hash.registry = upLinkKey;
1776
+ hash.url = URL.format(tarballUrl);
1777
+ }
1778
+ }
1779
+
1780
+ /**
1781
+ * Create or read a package.
1782
+ *
1783
+ * If the package already exists, it will be read.
1784
+ * If the package is not found, it will be created.
1785
+ * If the error is anything else will throw an error
1786
+ *
1787
+ * @param {*} pkgName
1788
+ * @param {*} callback
1789
+ * @return {Function}
1790
+ */
1791
+ private async readCreatePackage(pkgName: string): Promise<Manifest> {
1792
+ const storage: any = this.getPrivatePackageStorage(pkgName);
1793
+ if (_.isNil(storage)) {
1794
+ throw errorUtils.getInternalError('storage could not be found');
1795
+ }
1796
+
1797
+ try {
1798
+ const result: Manifest = await storage.readPackage(pkgName);
1799
+ return normalizePackage(result);
1800
+ } catch (err: any) {
1801
+ if (err.code === STORAGE.NO_SUCH_FILE_ERROR || err.code === HTTP_STATUS.NOT_FOUND) {
1802
+ return this._createNewPackageNext(pkgName);
1803
+ } else {
1804
+ this.logger.error(
1805
+ { err: err, file: STORAGE.PACKAGE_FILE_NAME },
1806
+ `'error reading' @{file}: @{!err.message}`
1807
+ );
858
1808
 
859
- // holds a "hidden" value to be used by the package storage.
860
- // $FlowFixMe
861
- version[Symbol.for('__verdaccio_uplink')] = upLink.upname;
1809
+ throw errorUtils.getInternalError();
862
1810
  }
863
1811
  }
864
1812
  }
1813
+
1814
+ /**
1815
+ Updates the local cache with the merge from the remote/client manifest.
1816
+
1817
+ The steps are the following.
1818
+ 1. Get the latest version of the package from the cache.
1819
+ 2. If does not exist will return a
1820
+
1821
+ @param name
1822
+ @param remoteManifest
1823
+ @returns return a merged manifest.
1824
+ */
1825
+ public async updateVersionsNext(name: string, remoteManifest: Manifest): Promise<Manifest> {
1826
+ debug(`updating versions for package %o`, name);
1827
+ let cacheManifest: Manifest = await this.readCreatePackage(name);
1828
+ let change = false;
1829
+ // updating readme
1830
+ cacheManifest.readme = getLatestReadme(remoteManifest);
1831
+ if (remoteManifest.readme !== cacheManifest.readme) {
1832
+ debug('manifest readme updated for %o', name);
1833
+ change = true;
1834
+ }
1835
+
1836
+ debug('updating new remote versions');
1837
+ for (const versionId in remoteManifest.versions) {
1838
+ // if detect a new remote version does not exist cache
1839
+ if (_.isNil(cacheManifest.versions[versionId])) {
1840
+ debug('new version from upstream %o', versionId);
1841
+ let version = remoteManifest.versions[versionId];
1842
+
1843
+ // we don't keep readme for package versions,
1844
+ // only one readme per package
1845
+ // TODO: readme clean up could be saved in configured eventually
1846
+ version = cleanUpReadme(version);
1847
+ debug('clean up readme for %o', versionId);
1848
+ version.contributors = normalizeContributors(version.contributors as Author[]);
1849
+
1850
+ change = true;
1851
+ cacheManifest.versions[versionId] = version;
1852
+
1853
+ if (version?.dist?.tarball) {
1854
+ const filename = pkgUtils.extractTarballName(version.dist.tarball);
1855
+ // store a fast access to the dist file by tarball name
1856
+ // it does NOT overwrite any existing records
1857
+ if (_.isNil(cacheManifest?._distfiles[filename])) {
1858
+ const hash: DistFile = (cacheManifest._distfiles[filename] = {
1859
+ url: version.dist.tarball,
1860
+ sha: version.dist.shasum,
1861
+ });
1862
+ // store cache metadata this the manifest
1863
+ const upLink: string = version[Symbol.for('__verdaccio_uplink')];
1864
+ if (_.isNil(upLink) === false) {
1865
+ this.updateUplinkToRemoteProtocol(hash, upLink);
1866
+ }
1867
+ }
1868
+ }
1869
+ } else {
1870
+ debug('no new versions from upstream %s', name);
1871
+ }
1872
+ }
1873
+
1874
+ debug('update dist-tags');
1875
+ for (const tag in remoteManifest[DIST_TAGS]) {
1876
+ if (
1877
+ !cacheManifest[DIST_TAGS][tag] ||
1878
+ cacheManifest[DIST_TAGS][tag] !== remoteManifest[DIST_TAGS][tag]
1879
+ ) {
1880
+ change = true;
1881
+ cacheManifest[DIST_TAGS][tag] = remoteManifest[DIST_TAGS][tag];
1882
+ }
1883
+ }
1884
+
1885
+ for (const up in remoteManifest._uplinks) {
1886
+ if (Object.prototype.hasOwnProperty.call(remoteManifest._uplinks, up)) {
1887
+ const need_change =
1888
+ !isObject(cacheManifest._uplinks[up]) ||
1889
+ remoteManifest._uplinks[up].etag !== cacheManifest._uplinks[up].etag ||
1890
+ remoteManifest._uplinks[up].fetched !== cacheManifest._uplinks[up].fetched;
1891
+
1892
+ if (need_change) {
1893
+ change = true;
1894
+ cacheManifest._uplinks[up] = remoteManifest._uplinks[up];
1895
+ }
1896
+ }
1897
+ }
1898
+
1899
+ debug('update time');
1900
+ if ('time' in remoteManifest && !_.isEqual(cacheManifest.time, remoteManifest.time)) {
1901
+ cacheManifest.time = remoteManifest.time;
1902
+ change = true;
1903
+ }
1904
+
1905
+ if (change) {
1906
+ debug('updating package info %o', name);
1907
+ await this.writePackageNext(name, cacheManifest);
1908
+ return cacheManifest;
1909
+ } else {
1910
+ return cacheManifest;
1911
+ }
1912
+ }
865
1913
  }
866
1914
 
867
1915
  export { Storage };