feat(cli): resolve storage path from env (#9147)

* feat(cli): resolve storage path from env

* fix: storage path

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: improve storage path

* fix: storage path

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
chenos
2026-04-18 22:40:50 +08:00
committed by GitHub
parent 0e810ddcb2
commit 137fd1b71a
33 changed files with 191 additions and 90 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
*/
import { Context, Next } from '@nocobase/actions';
import { Registry } from '@nocobase/utils';
import { Registry, storagePathJoin } from '@nocobase/utils';
import { Auth, AuthExtend } from './auth';
import { JwtOptions, JwtService } from './base/jwt-service';
import { ITokenBlacklistService } from './base/token-blacklist-service';
@@ -155,7 +155,7 @@ export class AuthManager {
if (process.env.UNSAFE_USE_DEFAULT_JWT_SECRET === 'true') {
return process.env.APP_KEY;
}
const jwtSecretPath = path.resolve(process.cwd(), 'storage', 'apps', 'main', 'jwt_secret.dat');
const jwtSecretPath = storagePathJoin('apps', 'main', 'jwt_secret.dat');
const jwtSecretExists = fs.existsSync(jwtSecretPath);
if (jwtSecretExists) {
const key = fs.readFileSync(jwtSecretPath);
+10 -1
View File
@@ -77,4 +77,13 @@ export const getCjsPackages = (packages: Package[]) =>
// tar
export const tarIncludesFiles = ['package.json', 'README.md', 'LICENSE', 'dist', '!node_modules'];
export const TAR_OUTPUT_DIR = process.env.TAR_PATH ? process.env.TAR_PATH : path.join(ROOT_PATH, 'storage', 'tar');
function resolveStorageRoot(): string {
const raw = process.env.STORAGE_PATH;
if (raw) {
return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
}
return path.join(ROOT_PATH, 'storage');
}
export const TAR_OUTPUT_DIR = process.env.TAR_PATH || path.join(resolveStorageRoot(), 'tar');
@@ -8,9 +8,9 @@
*/
const { resolve, posix } = require('path');
const { storagePathJoin, resolvePublicPath, resolveV2PublicPath } = require('../util');
const { Command } = require('commander');
const { readFileSync, writeFileSync } = require('fs');
const { resolvePublicPath, resolveV2PublicPath } = require('../util');
/**
*
@@ -47,7 +47,7 @@ module.exports = (cli) => {
.replace(/\{\{v2PublicPathNoTrailingSlash\}\}/g, v2PublicPathWithoutTrailingSlash)
.replace(/\{\{apiPort\}\}/g, process.env.APP_PORT)
.replace(/\{\{otherLocation\}\}/g, otherLocation);
const targetFile = resolve(process.cwd(), 'storage', 'nocobase.conf');
const targetFile = storagePathJoin('nocobase.conf');
writeFileSync(targetFile, replaced);
});
};
+2 -1
View File
@@ -9,6 +9,7 @@
const fs = require('fs');
const path = require('path');
const { storagePathJoin } = require('./util');
const winston = require('winston');
require('winston-daily-rotate-file');
@@ -62,7 +63,7 @@ function createSystemLogger({ dirname, filename, defaultMeta = {} }) {
}
const getLoggerFilePath = (...paths) => {
return path.resolve(process.env.LOGGER_BASE_PATH || path.resolve(process.cwd(), 'storage', 'logs'), ...paths);
return path.resolve(process.env.LOGGER_BASE_PATH || storagePathJoin('logs'), ...paths);
};
const logger = createSystemLogger({
+49 -10
View File
@@ -297,7 +297,7 @@ exports.genTsConfigPaths = function genTsConfigPaths() {
function generatePlaywrightPath(clean = false) {
try {
const playwright = resolve(process.cwd(), 'storage/playwright/tests');
const playwright = storagePathJoin('playwright', 'tests');
if (clean && fs.existsSync(playwright)) {
fs.rmSync(dirname(playwright), { force: true, recursive: true });
}
@@ -399,6 +399,39 @@ function areTimeZonesEqual(timeZone1, timeZone2) {
return moment.tz(timeZone1).format('Z') === moment.tz(timeZone2).format('Z');
}
/**
* Absolute application storage root (same rules as `@nocobase/utils` `resolveStorageRoot`).
* Resolves STORAGE_PATH for initEnv; after initEnv, `process.env.STORAGE_PATH` is set to this absolute path.
*/
function resolveStorageRoot() {
if (process.env.STORAGE_PATH) {
if (isAbsolute(process.env.STORAGE_PATH)) {
return process.env.STORAGE_PATH;
}
return resolve(process.cwd(), process.env.STORAGE_PATH);
}
return resolve(process.cwd(), 'storage');
}
/** Join segments under the storage root (same semantics as `@nocobase/utils` `storagePathJoin`). */
function storagePathJoin(...segments) {
return join(resolveStorageRoot(), ...segments);
}
exports.resolveStorageRoot = resolveStorageRoot;
exports.storagePathJoin = storagePathJoin;
/** @deprecated Use resolveStorageRoot — kept for backward compatibility */
exports.generateStoragePath = resolveStorageRoot;
/** Align with server `getPluginStoragePath()`: `PLUGIN_STORAGE_PATH` first, else `<STORAGE_PATH>/plugins`. */
function resolvePluginStoragePath() {
if (process.env.PLUGIN_STORAGE_PATH) {
const p = process.env.PLUGIN_STORAGE_PATH;
return isAbsolute(p) ? p : resolve(process.cwd(), p);
}
return storagePathJoin('plugins');
}
function generateGatewayPath() {
if (process.env.SOCKET_PATH) {
if (isAbsolute(process.env.SOCKET_PATH)) {
@@ -409,7 +442,7 @@ function generateGatewayPath() {
if (process.env.NOCOBASE_RUNNING_IN_DOCKER === 'true') {
return resolve(os.homedir(), '.nocobase', 'gateway.sock');
}
return resolve(process.cwd(), 'storage/gateway.sock');
return storagePathJoin('gateway.sock');
}
function generatePm2Home() {
@@ -422,7 +455,7 @@ function generatePm2Home() {
if (process.env.NOCOBASE_RUNNING_IN_DOCKER === 'true') {
return resolve(os.homedir(), '.nocobase', 'pm2');
}
return resolve(process.cwd(), './storage/.pm2');
return storagePathJoin('.pm2');
}
exports.initEnv = function initEnv() {
@@ -435,12 +468,10 @@ exports.initEnv = function initEnv() {
API_CLIENT_SHARE_TOKEN: 'false',
API_CLIENT_STORAGE_TYPE: 'localStorage',
// DB_DIALECT: 'sqlite',
DB_STORAGE: 'storage/db/nocobase.sqlite',
// DB_STORAGE, LOCAL_STORAGE_DEST, PLUGIN_STORAGE_PATH, etc. are set after dotenv from STORAGE_PATH
// DB_TIMEZONE: '+00:00',
DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'),
DEFAULT_STORAGE_TYPE: 'local',
LOCAL_STORAGE_DEST: 'storage/uploads',
PLUGIN_STORAGE_PATH: resolve(process.cwd(), 'storage/plugins'),
MFSU_AD: 'none',
MAKO_AD: 'none',
WS_PATH: '/ws',
@@ -449,17 +480,14 @@ exports.initEnv = function initEnv() {
NODE_MODULES_PATH: resolve(process.cwd(), 'node_modules'),
PLUGIN_PACKAGE_PREFIX: '@nocobase/plugin-,@nocobase/plugin-sample-,@nocobase/preset-',
SERVER_TSCONFIG_PATH: './tsconfig.server.json',
PLAYWRIGHT_AUTH_FILE: resolve(process.cwd(), 'storage/playwright/.auth/admin.json'),
CACHE_DEFAULT_STORE: 'memory',
CACHE_MEMORY_MAX: 2000,
BROWSERSLIST_IGNORE_OLD_DATA: true,
PLUGIN_STATICS_PATH: '/static/plugins/',
LOGGER_BASE_PATH: 'storage/logs',
APP_SERVER_BASE_URL: '',
APP_BASE_URL: '',
CDN_BASE_URL: '',
APP_PUBLIC_PATH: '/',
WATCH_FILE: resolve(process.cwd(), 'storage/app.watch.ts'),
ESM_CDN_BASE_URL: 'https://esm.sh',
ESM_CDN_SUFFIX: '',
};
@@ -491,6 +519,17 @@ exports.initEnv = function initEnv() {
path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'),
});
const storagePath = resolveStorageRoot();
process.env.STORAGE_PATH = storagePath; // absolute; other modules may use process.env.STORAGE_PATH after this
Object.assign(env, {
DB_STORAGE: storagePathJoin('db', 'nocobase.sqlite'),
LOCAL_STORAGE_DEST: storagePathJoin('uploads'),
PLUGIN_STORAGE_PATH: storagePathJoin('plugins'),
PLAYWRIGHT_AUTH_FILE: storagePathJoin('playwright', '.auth', 'admin.json'),
LOGGER_BASE_PATH: storagePathJoin('logs'),
WATCH_FILE: storagePathJoin('app.watch.ts'),
});
if (process.argv[2] === 'e2e' && !process.env.APP_BASE_URL) {
process.env.APP_BASE_URL = `http://127.0.0.1:${process.env.APP_PORT}`;
}
@@ -555,7 +594,7 @@ exports.initEnv = function initEnv() {
'@nocobase/plugin-workflow-response-message',
];
for (const pkg of pkgs) {
const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg);
const pkgDir = join(resolvePluginStoragePath(), pkg);
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true });
}
};
+1
View File
@@ -11,6 +11,7 @@
"directory": "packages/logger"
},
"dependencies": {
"@nocobase/utils": "2.1.0-alpha.18",
"chalk": "^4",
"lodash": "^4.17.21",
"triple-beam": "^1.4.1",
+2 -1
View File
@@ -8,12 +8,13 @@
*/
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
export const getLoggerLevel = () =>
process.env.LOGGER_LEVEL || (process.env.APP_ENV === 'development' ? 'debug' : 'info');
export const getLoggerFilePath = (...paths: string[]): string => {
return path.resolve(process.env.LOGGER_BASE_PATH || path.resolve(process.cwd(), 'storage', 'logs'), ...paths);
return path.resolve(process.env.LOGGER_BASE_PATH || storagePathJoin('logs'), ...paths);
};
export const getLoggerTransport = (): ('console' | 'file' | 'dailyRotateFile')[] =>
@@ -13,6 +13,7 @@ import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
import { MockServer, createMockServer, sleep } from '@nocobase/test';
import { storagePathJoin } from '@nocobase/utils';
import { Plugin } from '../plugin';
class MockPlugin extends Plugin {
@@ -324,7 +325,7 @@ describe('memory queue adapter', () => {
describe('storage', () => {
test('graceful shutdown, will create storage', async () => {
const mockListener = vi.fn();
const queueFile = path.resolve(process.cwd(), 'storage', 'apps', app.name, 'event-queue.json');
const queueFile = storagePathJoin('apps', app.name, 'event-queue.json');
await expect(fs.stat(queueFile)).rejects.toThrowError();
await app.eventQueue.subscribe('test1', {
idle: () => true,
+3 -2
View File
@@ -10,6 +10,7 @@
import crypto from 'crypto';
import fs from 'fs-extra';
import path, { resolve } from 'path';
import { storagePathJoin } from '@nocobase/utils';
import Application from './application';
export class AesEncryptor {
@@ -75,12 +76,12 @@ export class AesEncryptor {
}
static async getKeyPath(appName: string) {
const appKeyPath = path.resolve(process.cwd(), 'storage', 'apps', appName, 'aes_key.dat');
const appKeyPath = storagePathJoin('apps', appName, 'aes_key.dat');
const appKeyExists = await fs.exists(appKeyPath);
if (appKeyExists) {
return appKeyPath;
}
const envKeyPath = path.resolve(process.cwd(), 'storage', 'environment-variables', appName, 'aes_key.dat');
const envKeyPath = storagePathJoin('environment-variables', appName, 'aes_key.dat');
const envKeyExists = await fs.exists(envKeyPath);
if (envKeyExists) {
return envKeyPath;
@@ -10,6 +10,7 @@
import fg from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import { FlexSearchIndex } from '@nocobase/ai';
import type Application from '../application';
import { findAllPlugins } from '../plugin-manager/findPackageNames';
@@ -33,7 +34,7 @@ type DirectoryChildren = Map<
}
>;
const DOCS_STORAGE_DIR = path.resolve(process.cwd(), 'storage/ai/docs');
const DOCS_STORAGE_DIR = storagePathJoin('ai', 'docs');
const REFERENCE_START = '<!-- docs:references:start -->';
const REFERENCE_END = '<!-- docs:references:end -->';
const SPLIT_REFERENCE_START = '<!-- docs:splits:start -->';
+2 -2
View File
@@ -10,7 +10,7 @@
/* istanbul ignore file -- @preserve */
import fs from 'fs-extra';
import { resolve } from 'path';
import { storagePathJoin } from '@nocobase/utils';
import Application from '../application';
import { createDocsIndex } from '../ai/create-docs-index';
import { ApplicationNotInstall } from '../errors/application-not-install';
@@ -23,7 +23,7 @@ export default (app: Application) => {
.option('--quickstart')
.action(async (...cliArgs) => {
const [options] = cliArgs;
const file = resolve(process.cwd(), 'storage/.upgrading');
const file = storagePathJoin('.upgrading');
const upgrading = await fs.exists(file);
if (upgrading) {
if (!process.env.VITEST) {
+2 -2
View File
@@ -14,7 +14,7 @@ import fs from 'fs/promises';
import Application from './application';
import { SystemLogger } from '@nocobase/logger';
import { sleep } from '@nocobase/utils';
import { sleep, storagePathJoin } from '@nocobase/utils';
export const QUEUE_DEFAULT_INTERVAL = 250;
export const QUEUE_DEFAULT_CONCURRENCY = 1;
@@ -87,7 +87,7 @@ export class MemoryEventQueueAdapter implements IEventQueueAdapter {
}
private get storagePath() {
return path.resolve(process.cwd(), 'storage', 'apps', this.options.appName, 'event-queue.json');
return storagePathJoin('apps', this.options.appName, 'event-queue.json');
}
listen = (channel: string) => {
+11 -9
View File
@@ -8,7 +8,7 @@
*/
import { createSystemLogger, getLoggerFilePath, SystemLogger } from '@nocobase/logger';
import { Registry, Toposort, ToposortOptions, uid } from '@nocobase/utils';
import { Registry, storagePathJoin, Toposort, ToposortOptions, uid } from '@nocobase/utils';
import { lockdownSes } from '@nocobase/utils';
import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink';
import { Command } from 'commander';
@@ -19,6 +19,7 @@ import fs from 'fs';
import http, { IncomingMessage, ServerResponse } from 'http';
import compose from 'koa-compose';
import { promisify } from 'node:util';
import { homedir } from 'node:os';
import { extname, isAbsolute, resolve } from 'path';
import qs from 'qs';
import handler from 'serve-handler';
@@ -74,14 +75,16 @@ function normalizeBasePath(path = '') {
return normalized || '/';
}
/** Align with cli-v1 `generateGatewayPath()` / `process.env.SOCKET_PATH` after initEnv. */
function getSocketPath() {
const { SOCKET_PATH } = process.env;
if (isAbsolute(SOCKET_PATH)) {
return SOCKET_PATH;
const socketPath = process.env.SOCKET_PATH;
if (socketPath) {
return isAbsolute(socketPath) ? socketPath : resolve(process.cwd(), socketPath);
}
return resolve(process.cwd(), SOCKET_PATH);
if (process.env.NOCOBASE_RUNNING_IN_DOCKER === 'true') {
return resolve(homedir(), '.nocobase', 'gateway.sock');
}
return storagePathJoin('gateway.sock');
}
export class Gateway extends EventEmitter {
@@ -98,7 +101,7 @@ export class Gateway extends EventEmitter {
loggers = new Registry<SystemLogger>();
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
private host = '0.0.0.0';
private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock');
private socketPath = getSocketPath();
private v2IndexTemplateCache: { file: string; mtimeMs: number; html: string } | null = null;
private terminating = false;
@@ -136,7 +139,6 @@ export class Gateway extends EventEmitter {
private constructor() {
super();
this.reset();
this.socketPath = getSocketPath();
process.once('SIGTERM', this.onTerminate);
process.once('SIGINT', this.onTerminate);
}
@@ -8,6 +8,7 @@
*/
export const APP_NAME = 'nocobase';
/** Relative default under cwd when STORAGE_PATH / PLUGIN_STORAGE_PATH are unset; prefer `getPluginStoragePath()`. */
export const DEFAULT_PLUGIN_STORAGE_PATH = 'storage/plugins';
export const DEFAULT_PLUGIN_PATH = 'packages/plugins/';
export const pluginPrefix = (
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { uid } from '@nocobase/utils';
import { storagePathJoin, uid } from '@nocobase/utils';
import fs from 'fs';
import fse from 'fs-extra';
import path from 'path';
@@ -145,13 +145,13 @@ export default {
}
app.runAsCLI(['pm', 'add', values.packageName, ...args], { from: 'user' });
} else if (ctx.file) {
const tmpDir = path.resolve(process.cwd(), 'storage', 'tmp');
const tmpDir = storagePathJoin('tmp');
try {
await fs.promises.mkdir(tmpDir, { recursive: true });
} catch (error) {
// empty
}
const tempFile = path.join(process.cwd(), 'storage/tmp', uid() + path.extname(ctx.file.originalname));
const tempFile = path.join(tmpDir, uid() + path.extname(ctx.file.originalname));
await fs.promises.writeFile(tempFile, ctx.file.buffer, 'binary');
app.runAsCLI(['pm', 'add', tempFile], { from: 'user' });
} else if (values.compressedFileUrl) {
@@ -181,13 +181,13 @@ export default {
// }
if (ctx.file) {
values.packageName = ctx.request.body.packageName;
const tmpDir = path.resolve(process.cwd(), 'storage', 'tmp');
const tmpDir = storagePathJoin('tmp');
try {
await fs.promises.mkdir(tmpDir, { recursive: true });
} catch (error) {
// empty
}
const tempFile = path.join(process.cwd(), 'storage/tmp', uid() + path.extname(ctx.file.originalname));
const tempFile = path.join(tmpDir, uid() + path.extname(ctx.file.originalname));
await fs.promises.writeFile(tempFile, ctx.file.buffer, 'binary');
// args.push(`--url=${tempFile}`);
values.compressedFileUrl = tempFile;
@@ -9,7 +9,7 @@
import Topo from '@hapi/topo';
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
import { importModule, isURL } from '@nocobase/utils';
import { importModule, isURL, storagePathJoin } from '@nocobase/utils';
import execa from 'execa';
import fg from 'fast-glob';
import fs from 'fs-extra';
@@ -832,7 +832,7 @@ export class PluginManager {
if (process.env.VITEST) {
return;
}
const file = resolve(process.cwd(), 'storage/.upgrading');
const file = storagePathJoin('.upgrading');
this.app.log.debug('pending upgrade');
await fs.writeFile(file, 'upgrading');
};
@@ -940,7 +940,7 @@ export class PluginManager {
});
return;
}
const file = resolve(process.cwd(), 'storage/app-upgrading');
const file = storagePathJoin('app-upgrading');
await fs.writeFile(file, '', 'utf-8');
// await this.app.upgrade();
await tsxRerunning();
@@ -9,7 +9,7 @@
/* istanbul ignore next -- @preserve */
import { importModule, isURL, requireResolve } from '@nocobase/utils';
import { importModule, isURL, requireResolve, storagePathJoin } from '@nocobase/utils';
import { createStoragePluginSymLink } from '@nocobase/utils/plugin-symlink';
import axios, { AxiosRequestConfig } from 'axios';
import decompress from 'decompress';
@@ -21,15 +21,7 @@ import os from 'os';
import path from 'path';
import semver from 'semver';
import { getDepPkgPath, getPackageDir, getPackageFilePathWithExistCheck } from './clientStaticUtils';
import {
APP_NAME,
DEFAULT_PLUGIN_PATH,
DEFAULT_PLUGIN_STORAGE_PATH,
EXTERNAL,
importRegex,
pluginPrefix,
requireRegex,
} from './constants';
import { APP_NAME, DEFAULT_PLUGIN_PATH, EXTERNAL, importRegex, pluginPrefix, requireRegex } from './constants';
import deps from './deps';
import { PluginManagerRepository } from './plugin-manager-repository';
import { PluginData } from './types';
@@ -48,9 +40,13 @@ export async function getTempDir() {
return path.join(temporaryDirectory, APP_NAME);
}
/** Storage plugins directory: always prefer `PLUGIN_STORAGE_PATH` (absolute or relative to cwd). */
export function getPluginStoragePath() {
const pluginStoragePath = process.env.PLUGIN_STORAGE_PATH || DEFAULT_PLUGIN_STORAGE_PATH;
return path.isAbsolute(pluginStoragePath) ? pluginStoragePath : path.join(process.cwd(), pluginStoragePath);
if (process.env.PLUGIN_STORAGE_PATH) {
const pluginStoragePath = process.env.PLUGIN_STORAGE_PATH;
return path.isAbsolute(pluginStoragePath) ? pluginStoragePath : path.join(process.cwd(), pluginStoragePath);
}
return storagePathJoin('plugins');
}
export function getLocalPluginPackagesPathArr(): string[] {
+1
View File
@@ -50,6 +50,7 @@
"./vitest.mjs": "./vitest.mjs"
},
"dependencies": {
"@nocobase/utils": "2.1.0-alpha.18",
"@faker-js/faker": "8.1.0",
"@nocobase/server": "2.1.0-alpha.18",
"@playwright/test": "^1.45.3",
@@ -1,5 +1,6 @@
import { createMockServer } from '@nocobase/test';
import { CollectionRepository } from '@nocobase/plugin-data-source-main';
import { storagePathJoin } from '@nocobase/utils';
import fs from 'fs';
import path from 'path';
import os from 'os';
@@ -7,7 +8,7 @@ import ExcelJS from 'exceljs';
import { faker } from '@faker-js/faker';
import request from 'superagent';
const storagePath = path.resolve(process.cwd(), 'storage', 'perf', 'importDataTest');
const storagePath = storagePathJoin('perf', 'importDataTest');
export default async function main() {
const app = await createMockServer({
@@ -7,11 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import Path from 'node:path';
import { spawn, ChildProcess } from 'node:child_process';
import { getPortPromise } from 'portfinder';
import { uid } from '@nocobase/utils';
import { storagePathJoin, uid } from '@nocobase/utils';
import { createMockServer } from './mock-server';
type IsolatedClusterOptions = {
@@ -49,8 +48,8 @@ export class MockIsolatedCluster {
...this.options.env,
APP_PORT: `${port}`,
APPEND_PRESET_BUILT_IN_PLUGINS: (this.options.plugins ?? []).join(','),
SOCKET_PATH: `storage/tests/gateway-cluster-${uid()}.sock`,
PM2_HOME: Path.resolve(process.cwd(), `storage/tests/.pm2-${uid()}`),
SOCKET_PATH: storagePathJoin('tests', `gateway-cluster-${uid()}.sock`),
PM2_HOME: storagePathJoin('tests', `.pm2-${uid()}`),
},
});
+2 -3
View File
@@ -9,14 +9,13 @@
import { mockDatabase } from '@nocobase/database';
import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server';
import { uid } from '@nocobase/utils';
import { storagePathJoin, uid } from '@nocobase/utils';
import jwt from 'jsonwebtoken';
import qs from 'qs';
import supertest from 'supertest';
import { SuperAgent, SuperAgentRequest } from 'superagent';
import { MemoryPubSubAdapter } from './memory-pub-sub-adapter';
import { MockDataSource } from './mock-data-source';
import path from 'path';
import process from 'node:process';
import { promises as fs } from 'fs';
@@ -344,7 +343,7 @@ export async function createMockCluster({
export async function createMockServer(options: MockServerOptions = {}): Promise<MockServer> {
// clean cache directory
const cachePath = path.join(process.cwd(), 'storage', 'cache');
const cachePath = storagePathJoin('cache');
try {
await fs.rm(cachePath, { recursive: true, force: true });
await fs.mkdir(cachePath, { recursive: true });
+13 -3
View File
@@ -1,6 +1,16 @@
const { resolve } = require('path');
const path = require('path');
const { resolve } = path;
const fs = require('fs-extra');
/** Align with server `getPluginStoragePath()`: `PLUGIN_STORAGE_PATH` first, else `<STORAGE_PATH>/plugins`. */
function resolvePluginStoragePath() {
if (process.env.PLUGIN_STORAGE_PATH) {
const p = process.env.PLUGIN_STORAGE_PATH;
return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
}
return path.join(process.env.STORAGE_PATH || path.resolve(process.cwd(), 'storage'), 'plugins');
}
/**
* Recursively get plugin names from a directory
* @param {string} target - Target directory to scan
@@ -136,7 +146,7 @@ async function createPluginSymLink(pluginName, sourcePath, nodeModulesPath, plug
* @returns {Promise<void>}
*/
async function createStoragePluginSymLink(pluginName) {
const storagePluginsPath = resolve(process.cwd(), 'storage/plugins');
const storagePluginsPath = resolvePluginStoragePath();
const nodeModulesPath = process.env.NODE_MODULES_PATH;
await createPluginSymLink(pluginName, storagePluginsPath, nodeModulesPath, 'storage');
}
@@ -146,7 +156,7 @@ async function createStoragePluginSymLink(pluginName) {
* @returns {Promise<void>}
*/
async function createStoragePluginsSymlink() {
const storagePluginsPath = resolve(process.cwd(), 'storage/plugins');
const storagePluginsPath = resolvePluginStoragePath();
if (!(await fs.pathExists(storagePluginsPath))) {
return;
}
+1
View File
@@ -47,5 +47,6 @@ export * from './wrap-middleware';
export * from './run-sql';
export * from './liquidjs';
export * from './server-request';
export * from './storage-path';
export { lodash };
//
+33
View File
@@ -0,0 +1,33 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import path from 'path';
/**
* Absolute path to the application storage root (same rules as CLI `resolveStorageRoot` in `cli-v1/src/util.js`).
*/
export function resolveStorageRoot(): string {
const raw = process.env.STORAGE_PATH;
if (raw) {
return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
}
return path.resolve(process.cwd(), 'storage');
}
/**
* Join path segments under the application storage root.
* Resolution matches CLI `resolveStorageRoot()` / `initEnv`: use `STORAGE_PATH` when set
* (absolute or relative to cwd), otherwise `<cwd>/storage`.
*
* @example storagePathJoin('tmp')
* @example storagePathJoin('cache', 'apps', appName)
*/
export function storagePathJoin(...segments: string[]): string {
return path.join(resolveStorageRoot(), ...segments);
}
@@ -23,6 +23,7 @@ import os from 'os';
import { Logger } from '@nocobase/logger';
import _ from 'lodash';
import { Field, RelationField } from '@nocobase/database';
import { storagePathJoin } from '@nocobase/utils';
export type ExportOptions = {
collectionManager: ICollectionManager;
@@ -277,11 +278,7 @@ abstract class BaseExporter<T extends ExportOptions = ExportOptions> extends Eve
return value;
}
public generateOutputPath(
prefix = 'export',
ext = '',
destination = path.join(process.cwd(), 'storage', 'tmp'),
): string {
public generateOutputPath(prefix = 'export', ext = '', destination = storagePathJoin('tmp')): string {
const fileName = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`;
return path.join(destination, fileName);
}
@@ -9,12 +9,13 @@
import { z } from 'zod';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import fs from 'fs-extra';
import fg from 'fast-glob';
import { Index as FlexSearchIndex } from 'flexsearch';
import { ToolsOptions } from '@nocobase/ai';
const DEFAULT_DOCS_DIR = path.resolve(process.cwd(), 'storage/ai/docs');
const DEFAULT_DOCS_DIR = storagePathJoin('ai', 'docs');
type DocsIndexMeta = {
key: string;
@@ -15,6 +15,7 @@ import fse from 'fs-extra';
import fsPromises from 'fs/promises';
import { default as _, default as lodash } from 'lodash';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import * as process from 'process';
import stream from 'stream';
import util from 'util';
@@ -165,13 +166,10 @@ export class Dumper extends AppMigrator {
}
backUpStorageDir() {
const paths = [process.cwd(), 'storage', 'backups'];
if (this.app.name !== 'main') {
paths.push(this.app.name);
return storagePathJoin('backups', this.app.name);
}
return path.resolve(...paths);
return storagePathJoin('backups');
}
async allBackUpFilePaths(options?: { includeInProgress?: boolean; dir?: string }) {
@@ -11,6 +11,7 @@ import decompress from 'decompress';
import fs from 'fs';
import fsPromises from 'fs/promises';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import { AppMigrator, AppMigratorOptions } from './app-migrator';
import { readLines } from './utils';
import { Application } from '@nocobase/server';
@@ -100,7 +101,7 @@ export class Restorer extends AppMigrator {
if (path.isAbsolute(backUpFilePath)) {
this.backUpFilePath = backUpFilePath;
} else if (path.basename(backUpFilePath) === backUpFilePath) {
const dirname = path.resolve(process.cwd(), 'storage', 'duplicator');
const dirname = storagePathJoin('duplicator');
this.backUpFilePath = path.resolve(dirname, backUpFilePath);
} else {
this.backUpFilePath = path.resolve(process.cwd(), backUpFilePath);
@@ -12,7 +12,7 @@ import { Model, Transaction } from '@nocobase/database';
import { SequelizeCollectionManager } from '@nocobase/data-source-manager';
import { setCurrentRole } from '@nocobase/plugin-acl';
import { Application } from '@nocobase/server';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import PluginDataSourceManagerServer from '../plugin';
import { DataSourcesRolesModel } from './data-sources-roles-model';
@@ -110,7 +110,7 @@ export class DataSourceModel extends Model {
logger: app.logger.child({ dataSourceKey }),
sqlLogger: app.sqlLogger.child({ dataSourceKey }),
cache: app.cache,
storagePath: path.join(process.cwd(), 'storage', 'cache', 'apps', app.name),
storagePath: storagePathJoin('cache', 'apps', app.name),
databaseInstance,
});
@@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { isURL } from '@nocobase/utils';
import { isURL, resolveStorageRoot, storagePathJoin } from '@nocobase/utils';
import fsSync from 'fs';
import fs from 'fs/promises';
import multer from 'multer';
@@ -21,10 +21,14 @@ import { diskFilenameGetter } from '../utils';
const DEFAULT_BASE_URL = '/storage/uploads';
export function getDocumentRoot(storage): string {
const { documentRoot = process.env.LOCAL_STORAGE_DEST || path.join(process.cwd(), 'storage', 'uploads') } =
storage.options || {};
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
return path.resolve(path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot));
const storageRoot = resolveStorageRoot();
const raw = storage?.options?.documentRoot ?? process.env.LOCAL_STORAGE_DEST ?? storagePathJoin('uploads');
if (path.isAbsolute(raw)) {
return raw;
}
return path.resolve(process.cwd(), raw);
}
export function resolveSafePath(documentRoot: string, filePath?: string, filename?: string) {
@@ -13,6 +13,7 @@ import fs from 'node:fs';
import inject from 'light-my-request';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { storagePathJoin } from '@nocobase/utils';
import { createDbAdapter } from './db-adapter';
import { normalizeBasePath } from './utils';
@@ -325,7 +326,7 @@ export class IdpOauthService {
}
private getDefaultJwksPath(appName: string) {
return path.resolve(process.cwd(), 'storage', 'apps', appName, 'idp_oauth_jwks.json');
return storagePathJoin('apps', appName, 'idp_oauth_jwks.json');
}
private async getProviderSigningJwks(appName: string) {
@@ -9,10 +9,11 @@
import fs from 'fs';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import { exec } from 'child_process';
export async function getInstanceId() {
const dir = path.resolve(process.cwd(), 'storage/.license');
const dir = storagePathJoin('.license');
const filePath = path.resolve(dir, 'instance-id');
await createInstanceId(true);
const id = fs.readFileSync(filePath, 'utf-8');
@@ -32,7 +33,7 @@ export async function createInstanceId(force = false) {
}
export async function isLicenseKeyExists() {
const dir = path.resolve(process.cwd(), 'storage/.license');
const dir = storagePathJoin('.license');
const filePath = path.resolve(dir, 'license-key');
return fs.existsSync(filePath);
}
@@ -10,6 +10,7 @@
import { keyDecrypt } from '@nocobase/license-kit';
import { Context } from 'koa';
import path from 'path';
import { storagePathJoin } from '@nocobase/utils';
import fs from 'fs';
import { KeyData } from './interface';
import { CACHE_KEY } from './interface';
@@ -63,7 +64,7 @@ export async function request(
}
export async function saveLicenseKey(licenseKey: string, ctx?: any) {
const dir = path.resolve(process.cwd(), 'storage/.license');
const dir = storagePathJoin('.license');
const filePath = path.resolve(dir, 'license-key');
await fs.promises.writeFile(filePath, licenseKey);
@@ -74,7 +75,7 @@ export async function saveLicenseKey(licenseKey: string, ctx?: any) {
}
export async function getLocalKeyData() {
const keyFile = path.resolve(process.cwd(), 'storage/.license/license-key');
const keyFile = storagePathJoin('.license', 'license-key');
try {
const key = (await fs.promises.readFile(keyFile, 'utf8')).trim();
return JSON.parse(keyDecrypt(key));
@@ -85,7 +86,7 @@ export async function getLocalKeyData() {
export async function getKey(ctx?: Context): Promise<string> {
let key: string | undefined;
const keyFile = path.resolve(process.cwd(), 'storage/.license/license-key');
const keyFile = storagePathJoin('.license', 'license-key');
if (ctx?.cache) {
try {