Compare commits
27 Commits
v3.43.4
...
devcontain
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3231a27e59 | ||
|
|
072e146e5b | ||
|
|
8e1f4ca880 | ||
|
|
603bb82b1f | ||
|
|
0179ef364a | ||
|
|
f03d48e511 | ||
|
|
18ef7f49af | ||
|
|
9ea70088fb | ||
|
|
5bedb25f9d | ||
|
|
31946ba10e | ||
|
|
cde4f03c30 | ||
|
|
c3c7b391c0 | ||
|
|
079176926d | ||
|
|
e01d39ef4e | ||
|
|
22d401ee1f | ||
|
|
3cdb7cc9df | ||
|
|
5ad775e411 | ||
|
|
707a1d3809 | ||
|
|
9fcbb00406 | ||
|
|
413ec5685e | ||
|
|
f66057794e | ||
|
|
9a5e8c387a | ||
|
|
d24e3bd235 | ||
|
|
d6d72650dd | ||
|
|
4596b21271 | ||
|
|
c33f10e0ab | ||
|
|
88b6afb66e |
43
.devcontainer/devcontainer.json
Normal file
43
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,43 @@
|
||||
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
|
||||
{
|
||||
"name": "Node.js && Redis && MySQL",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Forwards ports
|
||||
"forwardPorts": [
|
||||
8080
|
||||
],
|
||||
"portsAttributes": {
|
||||
"8080": {
|
||||
"label": "Adminer",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": [
|
||||
"bash .devcontainer/scripts/postCreateCommand.sh"
|
||||
],
|
||||
|
||||
// Container Env
|
||||
"containerEnv": {
|
||||
"MYSQL_HOST": "mysql",
|
||||
"MYSQL_USER": "root",
|
||||
"MYSQL_PASSWORD": "root"
|
||||
},
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
}
|
||||
4
.devcontainer/scripts/postCreateCommand.sh
Executable file
4
.devcontainer/scripts/postCreateCommand.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
pnpm i
|
||||
socat TCP4-LISTEN:8080,reuseaddr,fork TCP:adminer:8080 &
|
||||
7
.github/workflows/codeql-analysis.yml
vendored
7
.github/workflows/codeql-analysis.yml
vendored
@@ -13,12 +13,9 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '41 13 * * 3'
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
||||
6
.github/workflows/nodejs.yml
vendored
6
.github/workflows/nodejs.yml
vendored
@@ -3,7 +3,11 @@
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test-mysql57-fs-nfs:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Node.js
|
||||
uses: node-modules/github-actions/.github/workflows/node-release.yml@master
|
||||
uses: cnpm/github-actions/.github/workflows/node-release.yml@master
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ lerna-debug.log*
|
||||
|
||||
.npmrc
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
config/config.prod.ts
|
||||
config/**/*.js
|
||||
|
||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,5 +1,90 @@
|
||||
# Changelog
|
||||
|
||||
## [3.48.3](https://github.com/cnpm/cnpmcore/compare/v3.48.2...v3.48.3) (2023-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* es query script score syntax fix and add error handler for 404 error ([#607](https://github.com/cnpm/cnpmcore/issues/607)) ([8e1f4ca](https://github.com/cnpm/cnpmcore/commit/8e1f4ca880c6ad09f766807e6a751b5ae960b550))
|
||||
|
||||
## [3.48.2](https://github.com/cnpm/cnpmcore/compare/v3.48.1...v3.48.2) (2023-11-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* should set OPTIONS on access-control-allow-methods ([#608](https://github.com/cnpm/cnpmcore/issues/608)) ([0179ef3](https://github.com/cnpm/cnpmcore/commit/0179ef364a5bc6aac5eafafee7136bf61405ee43))
|
||||
|
||||
## [3.48.1](https://github.com/cnpm/cnpmcore/compare/v3.48.0...v3.48.1) (2023-11-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* should set access-control-allow-origin and headers ([#606](https://github.com/cnpm/cnpmcore/issues/606)) ([18ef7f4](https://github.com/cnpm/cnpmcore/commit/18ef7f49affd2656cdba26fe21078f46ca1e0cc5))
|
||||
|
||||
## [3.48.0](https://github.com/cnpm/cnpmcore/compare/v3.47.2...v3.48.0) (2023-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow OPTIONS request on tgz downlaod url ([#605](https://github.com/cnpm/cnpmcore/issues/605)) ([5bedb25](https://github.com/cnpm/cnpmcore/commit/5bedb25f9dd29d684add0e1f4b827db0c8e0e818))
|
||||
|
||||
## [3.47.2](https://github.com/cnpm/cnpmcore/compare/v3.47.1...v3.47.2) (2023-10-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ignore BodyTimeoutError ([#603](https://github.com/cnpm/cnpmcore/issues/603)) ([cde4f03](https://github.com/cnpm/cnpmcore/commit/cde4f03c30dea074a32dc32f22f16c16c08fbe0d))
|
||||
|
||||
## [3.47.1](https://github.com/cnpm/cnpmcore/compare/v3.47.0...v3.47.1) (2023-10-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ignore HttpClientRequestTimeoutError on change stream worker ([#601](https://github.com/cnpm/cnpmcore/issues/601)) ([0791769](https://github.com/cnpm/cnpmcore/commit/079176926dd00c14cbf937d19a8e21dba8376a46))
|
||||
|
||||
## [3.47.0](https://github.com/cnpm/cnpmcore/compare/v3.46.0...v3.47.0) (2023-10-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ignore network error to error log ([#600](https://github.com/cnpm/cnpmcore/issues/600)) ([22d401e](https://github.com/cnpm/cnpmcore/commit/22d401ee1f103a9702448d4749f0028a676eddc0))
|
||||
|
||||
## [3.46.0](https://github.com/cnpm/cnpmcore/compare/v3.45.1...v3.46.0) (2023-10-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* read remote auth token from database ([#595](https://github.com/cnpm/cnpmcore/issues/595)) ([707a1d3](https://github.com/cnpm/cnpmcore/commit/707a1d3809f14cc9a7d613a16f8bea4e5baa7127))
|
||||
|
||||
## [3.45.1](https://github.com/cnpm/cnpmcore/compare/v3.45.0...v3.45.1) (2023-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use oss-cnpm@5.0.1 ([#597](https://github.com/cnpm/cnpmcore/issues/597)) ([413ec56](https://github.com/cnpm/cnpmcore/commit/413ec5685ee54dd90fcfcd5cb59a9b732ec73d84))
|
||||
|
||||
## [3.44.0](https://github.com/cnpm/cnpmcore/compare/v3.43.5...v3.44.0) (2023-10-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* sync all crhome for test binaries ([#592](https://github.com/cnpm/cnpmcore/issues/592)) ([4596b21](https://github.com/cnpm/cnpmcore/commit/4596b2127119f7c3c31f5fbe786504a7972d62a9))
|
||||
* use oss-client v2 ([#596](https://github.com/cnpm/cnpmcore/issues/596)) ([d24e3bd](https://github.com/cnpm/cnpmcore/commit/d24e3bd235fb73b1c145ff3b06dcc168d65b0f9f))
|
||||
|
||||
## [3.44.0](https://github.com/cnpm/cnpmcore/compare/v3.43.5...v3.44.0) (2023-09-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* sync all crhome for test binaries ([#592](https://github.com/cnpm/cnpmcore/issues/592)) ([4596b21](https://github.com/cnpm/cnpmcore/commit/4596b2127119f7c3c31f5fbe786504a7972d62a9))
|
||||
|
||||
## [3.43.5](https://github.com/cnpm/cnpmcore/compare/v3.43.4...v3.43.5) (2023-09-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* the license may be an object ([#587](https://github.com/cnpm/cnpmcore/issues/587)) ([88b6afb](https://github.com/cnpm/cnpmcore/commit/88b6afb66e77c825d24189f904c448a9cbb86fab)), closes [/github.com/cnpm/cnpmcore/issues/585#issuecomment-1706009496](https://github.com/cnpm//github.com/cnpm/cnpmcore/issues/585/issues/issuecomment-1706009496)
|
||||
|
||||
## [3.43.4](https://github.com/cnpm/cnpmcore/compare/v3.43.3...v3.43.4) (2023-09-02)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ WORKDIR /usr/src/app
|
||||
# Install app dependencies
|
||||
COPY . .
|
||||
|
||||
RUN npm install -g npminstall --registry=https://registry.npmmirror.com \
|
||||
# NPM Mirror
|
||||
# npm install -g npminstall --registry=https://registry.npmmirror.com
|
||||
# apk add --no-cache socat \
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install socat \
|
||||
&& npm install -g npminstall \
|
||||
&& npminstall -c \
|
||||
&& npm run tsc
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ async function _downloadToTempfile(httpclient: EggContextHttpClient,
|
||||
try {
|
||||
// max 10 mins to download
|
||||
// FIXME: should show download progress
|
||||
const authorization = optionalConfig?.remoteAuthToken ? `Bearer ${optionalConfig?.remoteAuthToken}` : '';
|
||||
const authorization = optionalConfig?.remoteAuthToken ? `Bearer ${optionalConfig.remoteAuthToken}` : '';
|
||||
const { status, headers, res } = await httpclient.request(url, {
|
||||
timeout: 60000 * 10,
|
||||
headers: { authorization },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { basename } from 'path';
|
||||
import { SingletonProto } from '@eggjs/tegg';
|
||||
import { BinaryType } from '../../enum/Binary';
|
||||
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
|
||||
@@ -5,6 +6,8 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
|
||||
@SingletonProto()
|
||||
@BinaryAdapter(BinaryType.ChromeForTesting)
|
||||
export class ChromeForTestingBinary extends AbstractBinary {
|
||||
static lastTimestamp = '';
|
||||
|
||||
private dirItems?: {
|
||||
[key: string]: BinaryItem[];
|
||||
};
|
||||
@@ -13,57 +16,102 @@ export class ChromeForTestingBinary extends AbstractBinary {
|
||||
this.dirItems = undefined;
|
||||
}
|
||||
|
||||
async fetch(dir: string): Promise<FetchResult | undefined> {
|
||||
if (!this.dirItems) {
|
||||
this.dirItems = {};
|
||||
this.dirItems['/'] = [];
|
||||
let chromeVersion = '';
|
||||
async #syncDirItems() {
|
||||
this.dirItems = {};
|
||||
this.dirItems['/'] = [];
|
||||
const jsonApiEndpoint = 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json';
|
||||
const { data } = await this.httpclient.request(jsonApiEndpoint, {
|
||||
dataType: 'json',
|
||||
timeout: 30000,
|
||||
followRedirect: true,
|
||||
gzip: true,
|
||||
});
|
||||
if (data.timestamp === ChromeForTestingBinary.lastTimestamp) return;
|
||||
|
||||
// exports.PUPPETEER_REVISIONS = Object.freeze({
|
||||
// chrome: '113.0.5672.63',
|
||||
// firefox: 'latest',
|
||||
// });
|
||||
const unpkgURL = 'https://unpkg.com/puppeteer-core@latest/lib/cjs/puppeteer/revisions.js';
|
||||
const text = await this.requestXml(unpkgURL);
|
||||
const m = /chrome:\s+\'([\d\.]+)\'\,/.exec(text);
|
||||
if (m) {
|
||||
chromeVersion = m[1];
|
||||
}
|
||||
|
||||
const platforms = [ 'linux64', 'mac-arm64', 'mac-x64', 'win32', 'win64' ];
|
||||
const date = new Date().toISOString();
|
||||
// "timestamp": "2023-09-16T00:21:21.964Z",
|
||||
// "versions": [
|
||||
// {
|
||||
// "version": "113.0.5672.0",
|
||||
// "revision": "1121455",
|
||||
// "downloads": {
|
||||
// "chrome": [
|
||||
// {
|
||||
// "platform": "linux64",
|
||||
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/linux64/chrome-linux64.zip"
|
||||
// },
|
||||
// {
|
||||
// "platform": "mac-arm64",
|
||||
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-arm64/chrome-mac-arm64.zip"
|
||||
// },
|
||||
// {
|
||||
// "platform": "mac-x64",
|
||||
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-x64/chrome-mac-x64.zip"
|
||||
// },
|
||||
// {
|
||||
// "platform": "win32",
|
||||
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win32/chrome-win32.zip"
|
||||
// },
|
||||
// {
|
||||
// "platform": "win64",
|
||||
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win64/chrome-win64.zip"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// },
|
||||
const versions = data.versions as {
|
||||
version: string;
|
||||
revision: string;
|
||||
downloads: {
|
||||
[key: string]: {
|
||||
platform: string;
|
||||
url: string;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
for (const item of versions) {
|
||||
this.dirItems['/'].push({
|
||||
name: `${chromeVersion}/`,
|
||||
date,
|
||||
name: `${item.version}/`,
|
||||
date: item.revision,
|
||||
size: '-',
|
||||
isDir: true,
|
||||
url: '',
|
||||
});
|
||||
this.dirItems[`/${chromeVersion}/`] = [];
|
||||
|
||||
for (const platform of platforms) {
|
||||
this.dirItems[`/${chromeVersion}/`].push({
|
||||
name: `${platform}/`,
|
||||
date,
|
||||
size: '-',
|
||||
isDir: true,
|
||||
url: '',
|
||||
});
|
||||
|
||||
// https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.63/mac-arm64/chrome-mac-arm64.zip
|
||||
const name = `chrome-${platform}.zip`;
|
||||
this.dirItems[`/${chromeVersion}/${platform}/`] = [
|
||||
{
|
||||
name,
|
||||
date,
|
||||
const versionDir = `/${item.version}/`;
|
||||
if (!this.dirItems[versionDir]) {
|
||||
this.dirItems[versionDir] = [];
|
||||
}
|
||||
for (const category in item.downloads) {
|
||||
const downloads = item.downloads[category];
|
||||
for (const download of downloads) {
|
||||
const platformDir = `${versionDir}${download.platform}/`;
|
||||
if (!this.dirItems[platformDir]) {
|
||||
this.dirItems[platformDir] = [];
|
||||
this.dirItems[versionDir].push({
|
||||
name: `${download.platform}/`,
|
||||
date: item.revision,
|
||||
size: '-',
|
||||
isDir: true,
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
this.dirItems[platformDir].push({
|
||||
name: basename(download.url),
|
||||
date: data.timestamp,
|
||||
size: '-',
|
||||
isDir: false,
|
||||
url: `https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${chromeVersion}/${platform}/${name}`,
|
||||
},
|
||||
];
|
||||
url: download.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ChromeForTestingBinary.lastTimestamp = data.timestamp;
|
||||
}
|
||||
|
||||
return { items: this.dirItems[dir], nextParams: null };
|
||||
async fetch(dir: string): Promise<FetchResult | undefined> {
|
||||
// use https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints
|
||||
if (!this.dirItems) {
|
||||
await this.#syncDirItems();
|
||||
}
|
||||
return { items: this.dirItems![dir], nextParams: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface RegistryData extends EntityData {
|
||||
changeStream: string;
|
||||
userPrefix: string;
|
||||
type: RegistryType;
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
export type CreateRegistryData = Omit<EasyData<RegistryData, 'registryId'>, 'id'>;
|
||||
@@ -20,6 +21,7 @@ export class Registry extends Entity {
|
||||
changeStream: string;
|
||||
userPrefix: string;
|
||||
type: RegistryType;
|
||||
authToken?: string;
|
||||
|
||||
constructor(data: RegistryData) {
|
||||
super(data);
|
||||
@@ -29,10 +31,11 @@ export class Registry extends Entity {
|
||||
this.changeStream = data.changeStream;
|
||||
this.userPrefix = data.userPrefix;
|
||||
this.type = data.type;
|
||||
this.authToken = data.authToken;
|
||||
}
|
||||
|
||||
public static create(data: CreateRegistryData): Registry {
|
||||
const newData = EntityUtil.defaultData(data, 'registryId');
|
||||
const newData = EntityUtil.defaultData<RegistryData, 'registryId'>(data, 'registryId');
|
||||
return new Registry(newData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface TaskData<T = TaskBaseData> extends EntityData {
|
||||
export type SyncPackageTaskOptions = {
|
||||
authorId?: string;
|
||||
authorIp?: string;
|
||||
remoteAuthToken?: string;
|
||||
tips?: string;
|
||||
skipDependencies?: boolean;
|
||||
syncDownloadData?: boolean;
|
||||
@@ -52,7 +51,6 @@ export interface TriggerHookTaskData extends TaskBaseData {
|
||||
}
|
||||
|
||||
export interface CreateSyncPackageTaskData extends TaskBaseData {
|
||||
remoteAuthToken?: string;
|
||||
tips?: string;
|
||||
skipDependencies?: boolean;
|
||||
syncDownloadData?: boolean;
|
||||
@@ -133,7 +131,6 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
|
||||
data: {
|
||||
// task execute worker
|
||||
taskWorker: '',
|
||||
remoteAuthToken: options?.remoteAuthToken,
|
||||
tips: options?.tips,
|
||||
registryId: options?.registryId ?? '',
|
||||
skipDependencies: options?.skipDependencies,
|
||||
|
||||
@@ -146,9 +146,17 @@ export class BinarySyncerService extends AbstractService {
|
||||
task.error = err.message;
|
||||
logs.push(`[${isoNow()}] ❌ Synced "${binaryName}" fail, ${task.error}, log: ${logUrl}`);
|
||||
logs.push(`[${isoNow()}] ❌❌❌❌❌ "${binaryName}" ❌❌❌❌❌`);
|
||||
this.logger.error('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
|
||||
task.taskId, task.targetName, task.error);
|
||||
this.logger.error(err);
|
||||
if (err.name === 'HttpClientRequestTimeoutError'
|
||||
|| err.name === 'ConnectionError'
|
||||
|| err.name === 'ConnectTimeoutError') {
|
||||
this.logger.warn('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
|
||||
task.taskId, task.targetName, task.error);
|
||||
this.logger.warn(err);
|
||||
} else {
|
||||
this.logger.error('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
|
||||
task.taskId, task.targetName, task.error);
|
||||
this.logger.error(err);
|
||||
}
|
||||
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
|
||||
}
|
||||
}
|
||||
@@ -210,7 +218,11 @@ export class BinarySyncerService extends AbstractService {
|
||||
this.logger.info('Not found %s, skip it', item.sourceUrl);
|
||||
logs.push(`[${isoNow()}][${dir}] 🧪️ [${parentIndex}${index}] Download ${item.sourceUrl} not found, skip it`);
|
||||
} else {
|
||||
this.logger.error('Download binary %s %s', item.sourceUrl, err);
|
||||
if (err.name === 'DownloadStatusInvalidError') {
|
||||
this.logger.warn('Download binary %s %s', item.sourceUrl, err);
|
||||
} else {
|
||||
this.logger.error('Download binary %s %s', item.sourceUrl, err);
|
||||
}
|
||||
hasDownloadError = true;
|
||||
logs.push(`[${isoNow()}][${dir}] ❌ [${parentIndex}${index}] Download ${item.sourceUrl} error: ${err}`);
|
||||
}
|
||||
|
||||
@@ -101,8 +101,14 @@ export class ChangesStreamService extends AbstractService {
|
||||
await setTimeout(this.config.cnpmcore.checkChangesStreamInterval);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error('[ChangesStreamService.executeTask:error] %s, exit now', err);
|
||||
this.logger.error(err);
|
||||
this.logger.warn('[ChangesStreamService.executeTask:error] %s, exit now', err.message);
|
||||
if (err.name === 'HttpClientRequestTimeoutError'
|
||||
|| err.name === 'ConnectTimeoutError'
|
||||
|| err.name === 'BodyTimeoutError') {
|
||||
this.logger.warn(err);
|
||||
} else {
|
||||
this.logger.error(err);
|
||||
}
|
||||
task.error = `${err}`;
|
||||
await this.taskRepository.saveTask(task);
|
||||
await this.suspendSync();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import { estypes, errors } from '@elastic/elasticsearch';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { AbstractService } from '../../common/AbstractService';
|
||||
@@ -67,7 +67,7 @@ export class PackageSearchService extends AbstractService {
|
||||
keywords: manifest.keywords || [],
|
||||
versions: Object.keys(manifest.versions),
|
||||
description: manifest.description,
|
||||
license: manifest.license,
|
||||
license: typeof manifest.license === 'object' ? manifest.license?.type : manifest.license,
|
||||
maintainers: manifest.maintainers,
|
||||
author: formatAuthor(manifest.author),
|
||||
'dist-tags': manifest['dist-tags'],
|
||||
@@ -138,7 +138,16 @@ export class PackageSearchService extends AbstractService {
|
||||
}
|
||||
|
||||
async removePackage(fullname: string) {
|
||||
return await this.searchRepository.removePackage(fullname);
|
||||
try {
|
||||
return await this.searchRepository.removePackage(fullname);
|
||||
} catch (error) {
|
||||
// if the package does not exist, returns success
|
||||
if (error instanceof errors.ResponseError && error?.statusCode === 404) {
|
||||
this.logger.warn('[PackageSearchService.removePackage] remove package:%s not found', fullname);
|
||||
return fullname;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/npms-io/queries/blob/master/lib/search.js#L8C1-L78C2
|
||||
@@ -213,7 +222,7 @@ export class PackageSearchService extends AbstractService {
|
||||
private _buildScriptScore(params: { text: string | undefined, scoreEffect: number }) {
|
||||
// keep search simple, only download(popularity)
|
||||
const downloads = 'doc["downloads.all"].value';
|
||||
const source = `doc["package.name.raw"].value.equals("${params.text}") ? 100000 + ${downloads} : _score * Math.pow(${downloads}, ${params.scoreEffect})`;
|
||||
const source = `doc["package.name.raw"].value.equals(params.text) ? 100000 + ${downloads} : _score * Math.pow(${downloads}, params.scoreEffect)`;
|
||||
return {
|
||||
script: {
|
||||
source,
|
||||
|
||||
@@ -121,13 +121,13 @@ export class PackageSyncerService extends AbstractService {
|
||||
const start = '2011-01-01';
|
||||
const end = this.config.cnpmcore.syncDownloadDataMaxDate;
|
||||
const registry = this.config.cnpmcore.syncDownloadDataSourceRegistry;
|
||||
const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry);
|
||||
const logs: string[] = [];
|
||||
let downloads: { day: string; downloads: number }[];
|
||||
|
||||
logs.push(`[${isoNow()}][DownloadData] 🚧🚧🚧🚧🚧 Syncing "${fullname}" download data "${start}:${end}" on ${registry} 🚧🚧🚧🚧🚧`);
|
||||
const failEnd = '❌❌❌❌❌ 🚮 give up 🚮 ❌❌❌❌❌';
|
||||
try {
|
||||
const { remoteAuthToken } = task.data as SyncPackageTaskOptions;
|
||||
const { data, status, res } = await this.npmRegistry.getDownloadRanges(registry, fullname, start, end, { remoteAuthToken });
|
||||
downloads = data.downloads || [];
|
||||
logs.push(`[${isoNow()}][DownloadData] 🚧 HTTP [${status}] timing: ${JSON.stringify(res.timing)}, downloads: ${downloads.length}`);
|
||||
@@ -164,7 +164,7 @@ export class PackageSyncerService extends AbstractService {
|
||||
private async syncUpstream(task: Task) {
|
||||
const registry = this.npmRegistry.registry;
|
||||
const fullname = task.targetName;
|
||||
const { remoteAuthToken } = task.data as SyncPackageTaskOptions;
|
||||
const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry);
|
||||
let logs: string[] = [];
|
||||
let logId = '';
|
||||
logs.push(`[${isoNow()}][UP] 🚧🚧🚧🚧🚧 Waiting sync "${fullname}" task on ${registry} 🚧🚧🚧🚧🚧`);
|
||||
@@ -351,10 +351,11 @@ export class PackageSyncerService extends AbstractService {
|
||||
public async executeTask(task: Task) {
|
||||
const fullname = task.targetName;
|
||||
const [ scope, name ] = getScopeAndName(fullname);
|
||||
const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, remoteAuthToken, specificVersions } = task.data as SyncPackageTaskOptions;
|
||||
const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, specificVersions } = task.data as SyncPackageTaskOptions;
|
||||
let pkg = await this.packageRepository.findPackage(scope, name);
|
||||
const registry = await this.initSpecRegistry(task, pkg, scope);
|
||||
const registryHost = this.npmRegistry.registry;
|
||||
const remoteAuthToken = registry.authToken;
|
||||
let logs: string[] = [];
|
||||
if (tips) {
|
||||
logs.push(`[${isoNow()}] 👉👉👉👉👉 Tips: ${tips} 👈👈👈👈👈`);
|
||||
@@ -660,7 +661,11 @@ export class PackageSyncerService extends AbstractService {
|
||||
localFile = tmpfile;
|
||||
logs.push(`[${isoNow()}] 🚧 [${syncIndex}] HTTP content-length: ${headers['content-length']}, timing: ${JSON.stringify(timing)} => ${localFile}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error('Download tarball %s error: %s', tarball, err);
|
||||
if (err.name === 'DownloadNotFoundError' || err.name === 'DownloadStatusInvalidError') {
|
||||
this.logger.warn('Download tarball %s error: %s', tarball, err);
|
||||
} else {
|
||||
this.logger.error('Download tarball %s error: %s', tarball, err);
|
||||
}
|
||||
lastErrorMessage = `download tarball error: ${err}`;
|
||||
logs.push(`[${isoNow()}] ❌ [${syncIndex}] Synced version ${version} fail, ${lastErrorMessage}`);
|
||||
await this.taskService.appendTaskLog(task, logs.join('\n'));
|
||||
@@ -875,7 +880,6 @@ export class PackageSyncerService extends AbstractService {
|
||||
authorId: task.authorId,
|
||||
authorIp: task.authorIp,
|
||||
tips,
|
||||
remoteAuthToken,
|
||||
});
|
||||
logs.push(`[${isoNow()}] 📦 Add dependency "${dependencyName}" sync task: ${dependencyTask.taskId}, db id: ${dependencyTask.id}`);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import { Task } from '../entity/Task';
|
||||
import { ChangesStreamMode, PresetRegistryName } from '../../common/constants';
|
||||
import { RegistryType } from '../../common/enum/Registry';
|
||||
|
||||
export interface CreateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name'> {
|
||||
export interface CreateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name' | 'authToken' > {
|
||||
operatorId?: string;
|
||||
}
|
||||
export interface UpdateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name' | 'registryId'> {
|
||||
export interface UpdateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'type' | 'name' | 'authToken' > {
|
||||
operatorId?: string;
|
||||
}
|
||||
export interface RemoveRegistryCmd extends Pick<Registry, 'registryId'> {
|
||||
@@ -61,7 +61,7 @@ export class RegistryManagerService extends AbstractService {
|
||||
}
|
||||
|
||||
async createRegistry(createCmd: CreateRegistryCmd): Promise<Registry> {
|
||||
const { name, changeStream = '', host, userPrefix = '', type, operatorId = '-' } = createCmd;
|
||||
const { name, changeStream = '', host, userPrefix = '', type, operatorId = '-', authToken } = createCmd;
|
||||
this.logger.info('[RegistryManagerService.createRegistry:prepare] operatorId: %s, createCmd: %j', operatorId, createCmd);
|
||||
const registry = Registry.create({
|
||||
name,
|
||||
@@ -69,6 +69,7 @@ export class RegistryManagerService extends AbstractService {
|
||||
host,
|
||||
userPrefix,
|
||||
type,
|
||||
authToken,
|
||||
});
|
||||
await this.registryRepository.saveRegistry(registry);
|
||||
return registry;
|
||||
@@ -76,8 +77,8 @@ export class RegistryManagerService extends AbstractService {
|
||||
|
||||
// 更新部分 registry 信息
|
||||
// 不允许 userPrefix 字段变更
|
||||
async updateRegistry(updateCmd: UpdateRegistryCmd) {
|
||||
const { name, changeStream, host, type, registryId, operatorId = '-' } = updateCmd;
|
||||
async updateRegistry(registryId: string, updateCmd: UpdateRegistryCmd) {
|
||||
const { name, changeStream, host, type, operatorId = '-', authToken } = updateCmd;
|
||||
this.logger.info('[RegistryManagerService.updateRegistry:prepare] operatorId: %s, updateCmd: %j', operatorId, updateCmd);
|
||||
const registry = await this.registryRepository.findRegistryByRegistryId(registryId);
|
||||
if (!registry) {
|
||||
@@ -88,6 +89,7 @@ export class RegistryManagerService extends AbstractService {
|
||||
changeStream,
|
||||
host,
|
||||
type,
|
||||
authToken,
|
||||
});
|
||||
await this.registryRepository.saveRegistry(registry);
|
||||
}
|
||||
@@ -105,6 +107,10 @@ export class RegistryManagerService extends AbstractService {
|
||||
return await this.registryRepository.findRegistry(registryName);
|
||||
}
|
||||
|
||||
async findByRegistryHost(host?: string): Promise<Registry | null> {
|
||||
return host ? await this.registryRepository.findRegistryByRegistryHost(host) : null;
|
||||
}
|
||||
|
||||
// 删除 Registry 方法
|
||||
// 可选传入 operatorId 作为参数,用于记录操作人员
|
||||
// 同时删除对应的 scope 数据
|
||||
@@ -156,4 +162,12 @@ export class RegistryManagerService extends AbstractService {
|
||||
|
||||
}
|
||||
|
||||
async getAuthTokenByRegistryHost(host: string): Promise<string|undefined> {
|
||||
const registry = await this.findByRegistryHost(host);
|
||||
if (!registry) {
|
||||
return undefined;
|
||||
}
|
||||
return registry.authToken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ export class PackageSyncController extends AbstractController {
|
||||
|
||||
const params = {
|
||||
fullname,
|
||||
remoteAuthToken: data.remoteAuthToken,
|
||||
tips,
|
||||
skipDependencies: !!data.skipDependencies,
|
||||
syncDownloadData: !!data.syncDownloadData,
|
||||
@@ -97,7 +96,6 @@ export class PackageSyncController extends AbstractController {
|
||||
const task = await this.packageSyncerService.createTask(params.fullname, {
|
||||
authorIp: ctx.ip,
|
||||
authorId: authorized?.user.userId,
|
||||
remoteAuthToken: params.remoteAuthToken,
|
||||
tips: params.tips,
|
||||
skipDependencies: params.skipDependencies,
|
||||
syncDownloadData: params.syncDownloadData,
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
import { NotFoundError } from 'egg-errors';
|
||||
import { AbstractController } from './AbstractController';
|
||||
import { Static } from 'egg-typebox-validate/typebox';
|
||||
import { RegistryManagerService } from '../../core/service/RegistryManagerService';
|
||||
import { RegistryManagerService, UpdateRegistryCmd } from '../../core/service/RegistryManagerService';
|
||||
import { AdminAccess } from '../middleware/AdminAccess';
|
||||
import { ScopeManagerService } from '../../core/service/ScopeManagerService';
|
||||
import { RegistryCreateOptions, QueryPageOptions, RegistryCreateSyncOptions } from '../typebox';
|
||||
import { RegistryCreateOptions, QueryPageOptions, RegistryCreateSyncOptions, RegistryUpdateOptions } from '../typebox';
|
||||
|
||||
@HTTPController()
|
||||
export class RegistryController extends AbstractController {
|
||||
@@ -67,7 +67,7 @@ export class RegistryController extends AbstractController {
|
||||
async createRegistry(@Context() ctx: EggContext, @HTTPBody() registryOptions: Static<typeof RegistryCreateOptions>) {
|
||||
ctx.tValidate(RegistryCreateOptions, registryOptions);
|
||||
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'setting');
|
||||
const { name, changeStream, host, userPrefix = '', type } = registryOptions;
|
||||
const { name, changeStream, host, userPrefix = '', type, authToken } = registryOptions;
|
||||
await this.registryManagerService.createRegistry({
|
||||
name,
|
||||
changeStream,
|
||||
@@ -75,6 +75,7 @@ export class RegistryController extends AbstractController {
|
||||
userPrefix,
|
||||
operatorId: authorizedUser.userId,
|
||||
type,
|
||||
authToken,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -106,4 +107,29 @@ export class RegistryController extends AbstractController {
|
||||
await this.registryManagerService.remove({ registryId: id, operatorId: authorizedUser.userId });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@HTTPMethod({
|
||||
path: '/-/registry/:id',
|
||||
method: HTTPMethodEnum.PATCH,
|
||||
})
|
||||
@Middleware(AdminAccess)
|
||||
async updateRegistry(@Context() ctx: EggContext, @HTTPParam() id: string, @HTTPBody() updateRegistryOptions: Partial<UpdateRegistryCmd>) {
|
||||
ctx.tValidate(RegistryUpdateOptions, updateRegistryOptions);
|
||||
const registry = await this.registryManagerService.findByRegistryId(id);
|
||||
if (!registry) {
|
||||
throw new NotFoundError('registry not found');
|
||||
} else {
|
||||
const { name, changeStream, host, type, authToken } = registry;
|
||||
const _updateRegistryOptions = {
|
||||
name,
|
||||
changeStream,
|
||||
host,
|
||||
type,
|
||||
authToken,
|
||||
...updateRegistryOptions,
|
||||
};
|
||||
await this.registryManagerService.updateRegistry(registry.registryId, _updateRegistryOptions);
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@ export class DownloadPackageVersionTarController extends AbstractController {
|
||||
@Inject()
|
||||
private nfsAdapter: NFSAdapter;
|
||||
|
||||
// Support OPTIONS Request on tgz download
|
||||
@HTTPMethod({
|
||||
// GET /:fullname/-/:filenameWithVersion.tgz
|
||||
path: `/:fullname(${FULLNAME_REG_STRING})/-/:filenameWithVersion.tgz`,
|
||||
method: HTTPMethodEnum.OPTIONS,
|
||||
})
|
||||
async downloadForOptions(@Context() ctx: EggContext) {
|
||||
ctx.set('access-control-allow-origin', '*');
|
||||
ctx.set('access-control-allow-methods', 'GET,HEAD');
|
||||
ctx.status = 204;
|
||||
}
|
||||
|
||||
@HTTPMethod({
|
||||
// GET /:fullname/-/:filenameWithVersion.tgz
|
||||
path: `/:fullname(${FULLNAME_REG_STRING})/-/:filenameWithVersion.tgz`,
|
||||
|
||||
@@ -29,7 +29,20 @@ export class SyncBinaryWorker {
|
||||
this.logger.info('[SyncBinaryWorker:executeTask:start] taskId: %s, targetName: %s, attempts: %s, params: %j, updatedAt: %s, delay %sms',
|
||||
task.taskId, task.targetName, task.attempts, task.data, task.updatedAt,
|
||||
startTime - task.updatedAt.getTime());
|
||||
await this.binarySyncerService.executeTask(task);
|
||||
try {
|
||||
await this.binarySyncerService.executeTask(task);
|
||||
} catch (err) {
|
||||
const use = Date.now() - startTime;
|
||||
this.logger.warn('[SyncBinaryWorker:executeTask:error] taskId: %s, targetName: %s, use %sms, error: %s',
|
||||
task.taskId, task.targetName, use, err.message);
|
||||
if (err.name === 'ConnectTimeoutError'
|
||||
|| err.name === 'HttpClientRequestTimeoutError') {
|
||||
this.logger.warn(err);
|
||||
} else {
|
||||
this.logger.error(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const use = Date.now() - startTime;
|
||||
this.logger.info('[SyncBinaryWorker:executeTask:success] taskId: %s, targetName: %s, use %sms',
|
||||
task.taskId, task.targetName, use);
|
||||
|
||||
@@ -79,12 +79,6 @@ export const TagWithVersionRule = Type.Object({
|
||||
|
||||
export const SyncPackageTaskRule = Type.Object({
|
||||
fullname: Name,
|
||||
remoteAuthToken: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
maxLength: 200,
|
||||
}),
|
||||
),
|
||||
tips: Type.String({
|
||||
transform: [ 'trim' ],
|
||||
maxLength: 1024,
|
||||
@@ -210,35 +204,44 @@ export const RegistryCreateOptions = Type.Object({
|
||||
maxLength: 256,
|
||||
})),
|
||||
type: Type.Enum(RegistryType),
|
||||
authToken: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
maxLength: 256,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const RegistryUpdateOptions = Type.Object({
|
||||
name: Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 256,
|
||||
}),
|
||||
host: Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 4096,
|
||||
}),
|
||||
changeStream: Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 4096,
|
||||
}),
|
||||
userPrefix: Type.Optional(Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 256,
|
||||
})),
|
||||
type: Type.Enum(RegistryType),
|
||||
registryId: Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 256,
|
||||
}),
|
||||
name: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 256,
|
||||
}),
|
||||
),
|
||||
host: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 4096,
|
||||
}),
|
||||
),
|
||||
changeStream: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 4096,
|
||||
}),
|
||||
),
|
||||
type: Type.Optional(Type.Enum(RegistryType)),
|
||||
authToken: Type.Optional(
|
||||
Type.String({
|
||||
transform: [ 'trim' ],
|
||||
minLength: 1,
|
||||
maxLength: 256,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ScopeCreateOptions = Type.Object({
|
||||
|
||||
@@ -51,7 +51,7 @@ export type PackageJSONType = CnpmcorePatchInfo & {
|
||||
url?: string;
|
||||
email?: string;
|
||||
};
|
||||
license?: string;
|
||||
license?: LicenseType | string;
|
||||
author?: AuthorType | string;
|
||||
contributors?: ContributorType[] | string[];
|
||||
maintainers?: ContributorType[] | string[];
|
||||
@@ -131,6 +131,11 @@ export type AuthorType = {
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type LicenseType = {
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type ContributorType = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
|
||||
@@ -38,6 +38,14 @@ export class RegistryRepository extends AbstractRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
async findRegistryByRegistryHost(host: string): Promise<RegistryEntity | null> {
|
||||
const model = await this.Registry.findOne({ host });
|
||||
if (model) {
|
||||
return ModelConvertor.convertModelToEntity(model, RegistryEntity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async saveRegistry(registry: Registry) {
|
||||
if (registry.id) {
|
||||
const model = await this.Registry.findOne({ id: registry.id });
|
||||
|
||||
@@ -36,4 +36,7 @@ export class Registry extends Bone {
|
||||
@Attribute(DataTypes.STRING(256))
|
||||
type: RegistryType;
|
||||
|
||||
@Attribute(DataTypes.STRING(256), { name: 'auth_token' })
|
||||
authToken?: string;
|
||||
|
||||
}
|
||||
|
||||
@@ -881,6 +881,13 @@ const binaries = {
|
||||
npmPackageName: 'libpg-query',
|
||||
},
|
||||
},
|
||||
'fuse-t': {
|
||||
category: 'fuse-t',
|
||||
description: 'FUSE-T is a kext-less implementation of FUSE for macOS that uses NFS v4 local server instead of a kernel extension.',
|
||||
type: BinaryType.GitHub,
|
||||
repo: 'macos-fuse-t/fuse-t',
|
||||
distUrl: 'https://github.com/macos-fuse-t/fuse-t/releases',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type BinaryName = keyof typeof binaries;
|
||||
|
||||
@@ -100,6 +100,8 @@ export default (appInfo: EggAppConfig) => {
|
||||
return ctx.get('Origin');
|
||||
},
|
||||
credentials: true,
|
||||
// https://github.com/koajs/cors/blob/master/index.js#L10C57-L10C64
|
||||
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
|
||||
};
|
||||
|
||||
config.nfs = {
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
version: '3.6'
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
networks:
|
||||
- cnpm
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
# networks:
|
||||
# - cnpm
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: node
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
# command: redis-server --appendonly yes --requirepass cnpm
|
||||
@@ -7,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- cnpm-redis:/data
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6379
|
||||
networks:
|
||||
- cnpm
|
||||
|
||||
@@ -16,7 +41,7 @@ services:
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
# MYSQL_DATABASE: 'cnpmcore_unittest'
|
||||
MYSQL_USER: user
|
||||
@@ -25,33 +50,30 @@ services:
|
||||
- cnpm-mysql:/var/lib/mysql
|
||||
# - ./conf.d/mysql/:/etc/mysql/conf.d
|
||||
# - ./init.d/mysql/:/docker-entrypoint-initdb.d
|
||||
- ./sql:/docker-entrypoint-initdb.d/sql
|
||||
- ./prepare-database.sh:/docker-entrypoint-initdb.d/init.sh
|
||||
ports:
|
||||
- 3306:3306
|
||||
- 3306
|
||||
networks:
|
||||
- cnpm
|
||||
|
||||
# database explorer
|
||||
phpmyadmin:
|
||||
image: phpmyadmin
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: pass
|
||||
PMA_HOST: 'mysql'
|
||||
ports:
|
||||
- 8080:80
|
||||
networks:
|
||||
- cnpm
|
||||
ADMINER_DEFAULT_DB_HOST: 'mysql'
|
||||
depends_on:
|
||||
- mysql
|
||||
ports:
|
||||
- 8080
|
||||
networks:
|
||||
- cnpm
|
||||
|
||||
volumes:
|
||||
cnpm-redis:
|
||||
cnpm-mysql:
|
||||
|
||||
|
||||
networks:
|
||||
cnpm:
|
||||
name: cnpm
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cnpmcore",
|
||||
"version": "3.43.4",
|
||||
"version": "3.48.3",
|
||||
"description": "npm core",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
@@ -97,13 +97,13 @@
|
||||
"eggjs-elasticsearch": "^0.0.6",
|
||||
"fs-cnpm": "^2.4.0",
|
||||
"ioredis": "^5.3.1",
|
||||
"leoric": "^2.6.2",
|
||||
"leoric": "^2.11.5",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"npm-package-arg": "^10.1.0",
|
||||
"oss-cnpm": "^4.0.0",
|
||||
"oss-cnpm": "^5.0.1",
|
||||
"p-map": "^4.0.0",
|
||||
"s3-cnpmcore": "^1.1.2",
|
||||
"semver": "^7.3.5",
|
||||
@@ -120,22 +120,22 @@
|
||||
"@cnpmjs/npm-cli-login": "^1.1.0",
|
||||
"@elastic/elasticsearch-mock": "^2.0.0",
|
||||
"@simplewebauthn/typescript-types": "^7.0.0",
|
||||
"@types/lodash": "^4.14.196",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/npm-package-arg": "^6.1.1",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/tar": "^6.1.4",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/validate-npm-package-name": "^4.0.0",
|
||||
"coffee": "^5.4.0",
|
||||
"egg-bin": "^6.0.0",
|
||||
"egg-mock": "^5.10.4",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-config-egg": "^12.1.0",
|
||||
"eslint-config-egg": "^13.0.0",
|
||||
"git-contributor": "2",
|
||||
"typescript": "^5.0.4",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/lodash": "^4.14.196",
|
||||
"@types/npm-package-arg": "^6.1.1",
|
||||
"@types/validate-npm-package-name": "^4.0.0"
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"author": "killagu",
|
||||
"license": "MIT",
|
||||
|
||||
1
sql/3.46.0.sql
Normal file
1
sql/3.46.0.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `registries` ADD COLUMN `auth_token` varchar(256) DEFAULT NULL COMMENT 'registry auth token';
|
||||
@@ -49,7 +49,7 @@ export class TestUtil {
|
||||
host: process.env.MYSQL_HOST || '127.0.0.1',
|
||||
port: process.env.MYSQL_PORT || 3306,
|
||||
user: process.env.MYSQL_USER || 'root',
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
password: process.env.MYSQL_PASSWORD || '',
|
||||
multipleStatements: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import { app } from 'egg-mock/bootstrap';
|
||||
import { ChromeForTestingBinary } from '../../../../app/common/adapter/binary/ChromeForTestingBinary';
|
||||
import { TestUtil } from '../../../../test/TestUtil';
|
||||
|
||||
describe('test/common/adapter/binary/ChromeForTestingBinary.test.ts', () => {
|
||||
let binary: ChromeForTestingBinary;
|
||||
@@ -9,21 +10,43 @@ describe('test/common/adapter/binary/ChromeForTestingBinary.test.ts', () => {
|
||||
});
|
||||
describe('fetch()', () => {
|
||||
it('should work for chrome binary', async () => {
|
||||
assert.equal(ChromeForTestingBinary.lastTimestamp, '');
|
||||
app.mockHttpclient('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json', 'GET', {
|
||||
data: await TestUtil.readFixturesFile('chrome-for-testing/known-good-versions-with-downloads.json'),
|
||||
persist: false,
|
||||
});
|
||||
const result = await binary.fetch('/');
|
||||
const latestVersion = result?.items?.[0].name;
|
||||
const latestVersion = result!.items![result!.items.length - 1].name;
|
||||
assert(latestVersion);
|
||||
assert.equal(latestVersion, '119.0.6008.0/');
|
||||
|
||||
const platformRes = await binary.fetch(`/${latestVersion}`);
|
||||
const platforms = platformRes?.items.map(item => item.name);
|
||||
assert(platforms);
|
||||
assert.deepEqual(platforms, [ 'linux64/', 'mac-arm64/', 'mac-x64/', 'win32/', 'win64/' ]);
|
||||
|
||||
for (const platform of platforms) {
|
||||
const versionRes = await binary.fetch(`/${latestVersion}${platform}`);
|
||||
const versions = versionRes?.items.map(item => item.name);
|
||||
assert.equal(versions?.length, 1);
|
||||
assert.equal(versions?.length, 3);
|
||||
assert(versionRes?.items[0].name);
|
||||
assert.equal(versionRes?.items[0].isDir, false);
|
||||
assert.match(versionRes?.items[0].name, /^chrome\-/);
|
||||
assert.match(versionRes?.items[1].name, /^chromedriver\-/);
|
||||
assert.match(versionRes?.items[2].name, /^chrome\-headless\-shell\-/);
|
||||
}
|
||||
assert(ChromeForTestingBinary.lastTimestamp);
|
||||
});
|
||||
|
||||
it('should return empty when timestamp is not changed', async () => {
|
||||
assert(ChromeForTestingBinary.lastTimestamp);
|
||||
await binary.initFetch();
|
||||
app.mockHttpclient('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json', 'GET', {
|
||||
data: await TestUtil.readFixturesFile('chrome-for-testing/known-good-versions-with-downloads.json'),
|
||||
persist: false,
|
||||
});
|
||||
const result = await binary.fetch('/');
|
||||
assert.equal(result?.items.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,8 +428,10 @@ describe('test/core/service/PackageSyncerService/executeTask.test.ts', () => {
|
||||
assert(log.includes('] 📦 Add dependency "@resvg/resvg-js-win32-x64-msvc" sync task: '));
|
||||
});
|
||||
|
||||
it('should bring auth token when set remoteAuthToken', async () => {
|
||||
it('should bring auth token which in registry database.', async () => {
|
||||
const testToken = 'test-auth-token';
|
||||
const registry = await registryManagerService.ensureDefaultRegistry();
|
||||
await registryManagerService.updateRegistry(registry.registryId, { ...registry, authToken: testToken });
|
||||
const fullManifests = await TestUtil.readFixturesFile('registry.npmjs.org/foobar.json');
|
||||
const tgzBuffer1_0_0 = await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz');
|
||||
const tgzBuffer1_1_0 = await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.1.0.tgz');
|
||||
@@ -459,7 +461,7 @@ describe('test/core/service/PackageSyncerService/executeTask.test.ts', () => {
|
||||
persist: false,
|
||||
};
|
||||
});
|
||||
await packageSyncerService.createTask('foobar', { skipDependencies: true, remoteAuthToken: testToken });
|
||||
await packageSyncerService.createTask('foobar', { skipDependencies: true });
|
||||
const task = await packageSyncerService.findExecuteTask();
|
||||
assert(task);
|
||||
await packageSyncerService.executeTask(task);
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('test/core/service/RegistryManagerService/index.test.ts', () => {
|
||||
let queryRes = await registryManagerService.listRegistries({});
|
||||
const [ registry ] = queryRes.data;
|
||||
|
||||
await registryManagerService.updateRegistry({
|
||||
await registryManagerService.updateRegistry(registry.registryId, {
|
||||
...registry,
|
||||
name: 'custom3',
|
||||
});
|
||||
@@ -89,9 +89,8 @@ describe('test/core/service/RegistryManagerService/index.test.ts', () => {
|
||||
assert(queryRes.count === 1);
|
||||
const [ registry ] = queryRes.data;
|
||||
await assert.rejects(
|
||||
registryManagerService.updateRegistry({
|
||||
registryManagerService.updateRegistry('not_exist', {
|
||||
...registry,
|
||||
registryId: 'not_exist',
|
||||
name: 'boo',
|
||||
}),
|
||||
/not found/,
|
||||
|
||||
13101
test/fixtures/chrome-for-testing/known-good-versions-with-downloads.json
vendored
Normal file
13101
test/fixtures/chrome-for-testing/known-good-versions-with-downloads.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,12 @@ describe('test/port/controller/HomeController/cors.test.ts', () => {
|
||||
const res = await app.httpRequest()
|
||||
.get('/-/ping')
|
||||
.set('origin', 'https://www.test-cors.org');
|
||||
assert(res.status === 200);
|
||||
assert(res.body.pong === true);
|
||||
assert(res.headers.vary === 'Origin');
|
||||
assert(res.headers['access-control-allow-origin'] === 'https://www.test-cors.org');
|
||||
assert(res.headers['access-control-allow-credentials'] === 'true');
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.pong, true);
|
||||
assert.equal(res.headers.vary, 'Origin');
|
||||
assert.equal(res.headers['access-control-allow-origin'], 'https://www.test-cors.org');
|
||||
assert.equal(res.headers['access-control-allow-credentials'], 'true');
|
||||
assert(!res.headers['access-control-allow-methods']);
|
||||
});
|
||||
|
||||
it('should OPTIONS work', async () => {
|
||||
@@ -20,11 +21,12 @@ describe('test/port/controller/HomeController/cors.test.ts', () => {
|
||||
.set('origin', 'https://www.test-cors.org/foo')
|
||||
.set('Access-Control-Request-Method', 'OPTIONS')
|
||||
.set('Access-Control-Request-Headers', 'authorization');
|
||||
assert(res.status === 204);
|
||||
assert(res.headers.vary === 'Origin');
|
||||
assert(res.headers['access-control-allow-origin'] === 'https://www.test-cors.org/foo');
|
||||
assert(res.headers['access-control-allow-credentials'] === 'true');
|
||||
assert(res.headers['access-control-allow-headers'] === 'authorization');
|
||||
assert.equal(res.status, 204);
|
||||
assert.equal(res.headers.vary, 'Origin');
|
||||
assert.equal(res.headers['access-control-allow-origin'], 'https://www.test-cors.org/foo');
|
||||
assert.equal(res.headers['access-control-allow-credentials'], 'true');
|
||||
assert.equal(res.headers['access-control-allow-headers'], 'authorization');
|
||||
assert.equal(res.headers['access-control-allow-methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,4 +245,36 @@ describe('test/port/controller/RegistryController/index.test.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('[PATCH /-/registry/:id] updateRegistry()', () => {
|
||||
it('should 403', async () => {
|
||||
await app.httpRequest()
|
||||
.patch(`/-/registry/${registry.registryId}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should 404 when not found', async () => {
|
||||
await app.httpRequest()
|
||||
.patch('/-/registry/registry-not-exists')
|
||||
.set('authorization', adminUser.authorization)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should update auth token success', async () => {
|
||||
await app.httpRequest()
|
||||
.patch(`/-/registry/${registry.registryId}`)
|
||||
.set('authorization', adminUser.authorization)
|
||||
.send({
|
||||
authToken: 'testAuthToekn',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const registList = await app.httpRequest()
|
||||
.get('/-/registry')
|
||||
.expect(200);
|
||||
|
||||
const latestToken = await registList.body.data[0].authToken;
|
||||
assert.equal(latestToken, 'testAuthToekn');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -53,6 +53,23 @@ describe('test/port/controller/package/DownloadPackageVersionTarController.test.
|
||||
assert(res.headers.location === `https://cdn.mock.com/packages/${scopedName}/1.0.0/${name}-1.0.0.tgz`);
|
||||
});
|
||||
|
||||
it('should support cors OPTIONS Request', async () => {
|
||||
mock(nfsClientAdapter, 'url', async (storeKey: string) => {
|
||||
return `https://cdn.mock.com${storeKey}`;
|
||||
});
|
||||
|
||||
let res = await app.httpRequest()
|
||||
.options(`/${name}/-/testmodule-download-version-tar-1.0.0.tgz`);
|
||||
assert.equal(res.status, 204);
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
assert.equal(res.headers['access-control-allow-methods'], 'GET,HEAD');
|
||||
res = await app.httpRequest()
|
||||
.options(`/${scopedName}/-/testmodule-download-version-tar-1.0.0.tgz`);
|
||||
assert.equal(res.status, 204);
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
assert.equal(res.headers['access-control-allow-methods'], 'GET,HEAD');
|
||||
});
|
||||
|
||||
if (process.env.CNPMCORE_NFS_TYPE !== 'oss') {
|
||||
it('should download a version tar redirect to mock cdn success with url function is not async function', async () => {
|
||||
mock(nfsClientAdapter, 'url', (storeKey: string) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { app, mock } from 'egg-mock/bootstrap';
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
|
||||
import { mockES } from '../../../../config/config.unittest';
|
||||
import { TestUtil } from '../../../TestUtil';
|
||||
@@ -123,5 +124,42 @@ describe('test/port/controller/package/SearchPackageController.test.ts', () => {
|
||||
.set('authorization', admin.authorization);
|
||||
assert.equal(res.body.package, name);
|
||||
});
|
||||
|
||||
it('should delete a non existent package', async () => {
|
||||
const name = 'non-existent-search-package';
|
||||
mockES.add({
|
||||
method: 'DELETE',
|
||||
path: `/${app.config.cnpmcore.elasticsearchIndex}/_doc/:id`,
|
||||
}, () => {
|
||||
return new errors.ResponseError({
|
||||
body: { errors: {}, status: 404 },
|
||||
statusCode: 404,
|
||||
warnings: null,
|
||||
meta: {
|
||||
name: '',
|
||||
context: '',
|
||||
request: {
|
||||
params: {
|
||||
method: 'delete',
|
||||
path: `/${app.config.cnpmcore.elasticsearchIndex}/_doc/:id`,
|
||||
},
|
||||
options: {},
|
||||
id: '',
|
||||
},
|
||||
connection: null,
|
||||
attempts: 1,
|
||||
aborted: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true);
|
||||
mock(app.config.cnpmcore, 'enableElasticsearch', true);
|
||||
mock(app.config.cnpmcore, 'registry', 'https://registry.example.com');
|
||||
|
||||
const res = await app.httpRequest()
|
||||
.delete(`/-/v1/search/sync/${name}`)
|
||||
.set('authorization', admin.authorization);
|
||||
assert.equal(res.body.package, name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('test/repository/RegistryRepository.test.ts', () => {
|
||||
changeStream: 'https://r.npmjs.com/_changes',
|
||||
host: 'https://registry.npmjs.org',
|
||||
type: 'cnpmcore' as RegistryType,
|
||||
authToken: '',
|
||||
})) as Registry;
|
||||
});
|
||||
|
||||
@@ -27,6 +28,7 @@ describe('test/repository/RegistryRepository.test.ts', () => {
|
||||
changeStream: 'https://ra.npmjs.com/_changes',
|
||||
host: 'https://registry.npmjs.org',
|
||||
type: 'cnpmcore' as RegistryType,
|
||||
authToken: '',
|
||||
})) as Registry;
|
||||
assert(newRegistry);
|
||||
assert(newRegistry.type === 'cnpmcore');
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"useUnknownInCatchVariables": false,
|
||||
},
|
||||
"exclude": [
|
||||
"test",
|
||||
"node_modules"
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user