create-forgeon 0.3.5 → 0.3.7
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.
- package/package.json +1 -1
- package/src/cli/add-help.mjs +1 -0
- package/src/cli/add-options.mjs +6 -0
- package/src/cli/add-options.test.mjs +2 -0
- package/src/modules/dependencies.mjs +31 -0
- package/src/modules/dependencies.test.mjs +207 -5
- package/src/modules/executor.mjs +14 -0
- package/src/modules/executor.test.mjs +752 -14
- package/src/modules/files-access.mjs +446 -0
- package/src/modules/files-image.mjs +540 -0
- package/src/modules/files-local.mjs +221 -0
- package/src/modules/files-quotas.mjs +402 -0
- package/src/modules/files-s3.mjs +266 -0
- package/src/modules/files.mjs +527 -0
- package/src/modules/queue.mjs +410 -0
- package/src/modules/registry.mjs +93 -3
- package/src/modules/shared/patch-utils.mjs +25 -0
- package/src/run-add-module.mjs +89 -2
- package/templates/module-fragments/files/00_title.md +1 -0
- package/templates/module-fragments/files/10_overview.md +17 -0
- package/templates/module-fragments/files/20_scope.md +13 -0
- package/templates/module-fragments/files/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-access/00_title.md +1 -0
- package/templates/module-fragments/files-access/10_overview.md +9 -0
- package/templates/module-fragments/files-access/20_scope.md +20 -0
- package/templates/module-fragments/files-access/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-image/00_title.md +1 -0
- package/templates/module-fragments/files-image/10_overview.md +10 -0
- package/templates/module-fragments/files-image/20_scope.md +20 -0
- package/templates/module-fragments/files-image/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-local/00_title.md +1 -0
- package/templates/module-fragments/files-local/10_overview.md +9 -0
- package/templates/module-fragments/files-local/20_scope.md +10 -0
- package/templates/module-fragments/files-local/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-quotas/00_title.md +1 -0
- package/templates/module-fragments/files-quotas/10_overview.md +9 -0
- package/templates/module-fragments/files-quotas/20_scope.md +20 -0
- package/templates/module-fragments/files-quotas/90_status_implemented.md +3 -0
- package/templates/module-fragments/files-s3/00_title.md +1 -0
- package/templates/module-fragments/files-s3/10_overview.md +17 -0
- package/templates/module-fragments/files-s3/20_scope.md +11 -0
- package/templates/module-fragments/files-s3/90_status_implemented.md +5 -0
- package/templates/module-fragments/queue/20_scope.md +8 -7
- package/templates/module-fragments/queue/90_status_implemented.md +3 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_record/migration.sql +30 -0
- package/templates/module-presets/files/apps/api/prisma/migrations/20260306_files_file_variant/migration.sql +55 -0
- package/templates/module-presets/files/packages/files/package.json +24 -0
- package/templates/module-presets/files/packages/files/src/dto/create-file.dto.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files-config.loader.ts +21 -0
- package/templates/module-presets/files/packages/files/src/files-config.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/files-config.service.ts +32 -0
- package/templates/module-presets/files/packages/files/src/files-env.schema.ts +30 -0
- package/templates/module-presets/files/packages/files/src/files.controller.ts +90 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +762 -0
- package/templates/module-presets/files/packages/files/src/files.types.ts +35 -0
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +12 -0
- package/templates/module-presets/files/packages/files/src/index.ts +9 -0
- package/templates/module-presets/files/packages/files/tsconfig.json +9 -0
- package/templates/module-presets/files-access/packages/files-access/package.json +17 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.service.ts +59 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.subject.ts +45 -0
- package/templates/module-presets/files-access/packages/files-access/src/files-access.types.ts +14 -0
- package/templates/module-presets/files-access/packages/files-access/src/forgeon-files-access.module.ts +8 -0
- package/templates/module-presets/files-access/packages/files-access/src/index.ts +4 -0
- package/templates/module-presets/files-access/packages/files-access/tsconfig.json +9 -0
- package/templates/module-presets/files-image/packages/files-image/package.json +21 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.loader.ts +32 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.module.ts +11 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-config.service.ts +55 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image-env.schema.ts +28 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.service.ts +420 -0
- package/templates/module-presets/files-image/packages/files-image/src/files-image.types.ts +18 -0
- package/templates/module-presets/files-image/packages/files-image/src/forgeon-files-image.module.ts +10 -0
- package/templates/module-presets/files-image/packages/files-image/src/index.ts +7 -0
- package/templates/module-presets/files-image/packages/files-image/tsconfig.json +9 -0
- package/templates/module-presets/files-local/packages/files-local/package.json +19 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.loader.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.module.ts +12 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-config.service.ts +11 -0
- package/templates/module-presets/files-local/packages/files-local/src/files-local-env.schema.ts +13 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +4 -0
- package/templates/module-presets/files-local/packages/files-local/tsconfig.json +9 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.loader.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-config.service.ts +27 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas-env.schema.ts +15 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.types.ts +22 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +11 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/index.ts +7 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/tsconfig.json +9 -0
- package/templates/module-presets/files-s3/packages/files-s3/package.json +20 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.loader.ts +57 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.module.ts +12 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-config.service.ts +44 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/files-s3-env.schema.ts +51 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +4 -0
- package/templates/module-presets/files-s3/packages/files-s3/tsconfig.json +9 -0
- package/templates/module-presets/queue/packages/queue/package.json +21 -0
- package/templates/module-presets/queue/packages/queue/src/forgeon-queue.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/index.ts +6 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.loader.ts +24 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.module.ts +10 -0
- package/templates/module-presets/queue/packages/queue/src/queue-config.service.ts +69 -0
- package/templates/module-presets/queue/packages/queue/src/queue-env.schema.ts +17 -0
- package/templates/module-presets/queue/packages/queue/src/queue.service.ts +88 -0
- package/templates/module-presets/queue/packages/queue/tsconfig.json +9 -0
- package/templates/module-fragments/queue/90_status_planned.md +0 -3
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureDependency,
|
|
7
|
+
ensureImportLine,
|
|
8
|
+
ensureLineAfter,
|
|
9
|
+
ensureLineBefore,
|
|
10
|
+
ensureLoadItem,
|
|
11
|
+
ensureValidatorSchema,
|
|
12
|
+
upsertEnvLines,
|
|
13
|
+
} from './shared/patch-utils.mjs';
|
|
14
|
+
|
|
15
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
16
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-local', relativePath);
|
|
17
|
+
if (!fs.existsSync(source)) {
|
|
18
|
+
throw new Error(`Missing files-local preset template: ${source}`);
|
|
19
|
+
}
|
|
20
|
+
const destination = path.join(targetRoot, relativePath);
|
|
21
|
+
copyRecursive(source, destination);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function patchApiPackage(targetRoot) {
|
|
25
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
26
|
+
if (!fs.existsSync(packagePath)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
31
|
+
ensureDependency(packageJson, '@forgeon/files-local', 'workspace:*');
|
|
32
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-local build']);
|
|
33
|
+
writeJson(packagePath, packageJson);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function patchAppModule(targetRoot) {
|
|
37
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
38
|
+
if (!fs.existsSync(filePath)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
43
|
+
content = ensureImportLine(
|
|
44
|
+
content,
|
|
45
|
+
"import { filesLocalConfig, filesLocalEnvSchemaZod, FilesLocalConfigModule } from '@forgeon/files-local';",
|
|
46
|
+
);
|
|
47
|
+
content = ensureLoadItem(content, 'filesLocalConfig');
|
|
48
|
+
content = ensureValidatorSchema(content, 'filesLocalEnvSchemaZod');
|
|
49
|
+
|
|
50
|
+
if (!content.includes(' FilesLocalConfigModule,')) {
|
|
51
|
+
if (content.includes(' ForgeonFilesModule,')) {
|
|
52
|
+
content = ensureLineAfter(content, ' ForgeonFilesModule,', ' FilesLocalConfigModule,');
|
|
53
|
+
} else if (content.includes(' ForgeonI18nModule.register({')) {
|
|
54
|
+
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' FilesLocalConfigModule,');
|
|
55
|
+
} else if (content.includes(' DbPrismaModule,')) {
|
|
56
|
+
content = ensureLineAfter(content, ' DbPrismaModule,', ' FilesLocalConfigModule,');
|
|
57
|
+
} else if (content.includes(' ForgeonLoggerModule,')) {
|
|
58
|
+
content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' FilesLocalConfigModule,');
|
|
59
|
+
} else {
|
|
60
|
+
content = ensureLineAfter(content, ' CoreErrorsModule,', ' FilesLocalConfigModule,');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function patchApiDockerfile(targetRoot) {
|
|
68
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
69
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
74
|
+
|
|
75
|
+
const packageAnchors = [
|
|
76
|
+
'COPY packages/files/package.json packages/files/package.json',
|
|
77
|
+
'COPY packages/auth-api/package.json packages/auth-api/package.json',
|
|
78
|
+
'COPY packages/rbac/package.json packages/rbac/package.json',
|
|
79
|
+
'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
|
|
80
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
81
|
+
'COPY packages/swagger/package.json packages/swagger/package.json',
|
|
82
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
83
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
84
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
85
|
+
];
|
|
86
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
87
|
+
content = ensureLineAfter(
|
|
88
|
+
content,
|
|
89
|
+
packageAnchor,
|
|
90
|
+
'COPY packages/files-local/package.json packages/files-local/package.json',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const sourceAnchors = [
|
|
94
|
+
'COPY packages/files packages/files',
|
|
95
|
+
'COPY packages/auth-api packages/auth-api',
|
|
96
|
+
'COPY packages/rbac packages/rbac',
|
|
97
|
+
'COPY packages/rate-limit packages/rate-limit',
|
|
98
|
+
'COPY packages/logger packages/logger',
|
|
99
|
+
'COPY packages/swagger packages/swagger',
|
|
100
|
+
'COPY packages/i18n packages/i18n',
|
|
101
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
102
|
+
'COPY packages/core packages/core',
|
|
103
|
+
];
|
|
104
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
105
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-local packages/files-local');
|
|
106
|
+
|
|
107
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/files-local build\r?\n?/gm, '');
|
|
108
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
109
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
110
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
111
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-local build');
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function patchCompose(targetRoot) {
|
|
117
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
118
|
+
if (!fs.existsSync(composePath)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
if (!content.includes('FILES_LOCAL_ROOT: ${FILES_LOCAL_ROOT}')) {
|
|
124
|
+
const anchors = [
|
|
125
|
+
/^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
|
|
126
|
+
/^(\s+FILES_STORAGE_DRIVER:.*)$/m,
|
|
127
|
+
/^(\s+FILES_ENABLED:.*)$/m,
|
|
128
|
+
/^(\s+API_PREFIX:.*)$/m,
|
|
129
|
+
];
|
|
130
|
+
const anchorPattern = anchors.find((pattern) => pattern.test(content)) ?? anchors.at(-1);
|
|
131
|
+
content = content.replace(
|
|
132
|
+
anchorPattern,
|
|
133
|
+
`$1
|
|
134
|
+
FILES_LOCAL_ROOT: \${FILES_LOCAL_ROOT}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!content.includes('files_data:/app/storage')) {
|
|
139
|
+
content = content.replace(
|
|
140
|
+
/^(\s{2}api:\n[\s\S]*?^\s{4}environment:\n(?:\s{6}.+\n)+)/m,
|
|
141
|
+
`$1 volumes:
|
|
142
|
+
- files_data:/app/storage
|
|
143
|
+
`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!/^volumes:\n/m.test(content)) {
|
|
148
|
+
content = `${content.trimEnd()}\n\nvolumes:\n files_data:\n`;
|
|
149
|
+
} else if (!/^\s{2}files_data:\s*$/m.test(content)) {
|
|
150
|
+
content = `${content.trimEnd()}\n files_data:\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function patchReadme(targetRoot) {
|
|
157
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
158
|
+
if (!fs.existsSync(readmePath)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const marker = '## Files Local Adapter Module';
|
|
163
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
164
|
+
if (content.includes(marker)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const section = `## Files Local Adapter Module
|
|
169
|
+
|
|
170
|
+
The files-local module provides the \`files-storage-adapter\` capability via local disk configuration.
|
|
171
|
+
|
|
172
|
+
Configuration:
|
|
173
|
+
- \`FILES_LOCAL_ROOT=storage/uploads\`
|
|
174
|
+
|
|
175
|
+
Notes:
|
|
176
|
+
- this adapter is used by \`@forgeon/files\` when \`FILES_STORAGE_DRIVER=local\`
|
|
177
|
+
- saved files are stored in \`FILES_LOCAL_ROOT\` relative to the project root
|
|
178
|
+
- Docker compose mounts named volume \`files_data\` to \`/app/storage\` for persistence`;
|
|
179
|
+
|
|
180
|
+
if (content.includes('## Prisma In Docker Start')) {
|
|
181
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
182
|
+
} else {
|
|
183
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function patchGitignore(targetRoot) {
|
|
190
|
+
const gitignorePath = path.join(targetRoot, '.gitignore');
|
|
191
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const marker = 'storage/';
|
|
196
|
+
let content = fs.readFileSync(gitignorePath, 'utf8').replace(/\r\n/g, '\n');
|
|
197
|
+
if (content.includes(marker)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
content = `${content.trimEnd()}\n${marker}\n`;
|
|
202
|
+
fs.writeFileSync(gitignorePath, content, 'utf8');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function applyFilesLocalModule({ packageRoot, targetRoot }) {
|
|
206
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-local'));
|
|
207
|
+
|
|
208
|
+
patchApiPackage(targetRoot);
|
|
209
|
+
patchAppModule(targetRoot);
|
|
210
|
+
patchApiDockerfile(targetRoot);
|
|
211
|
+
patchCompose(targetRoot);
|
|
212
|
+
patchReadme(targetRoot);
|
|
213
|
+
patchGitignore(targetRoot);
|
|
214
|
+
|
|
215
|
+
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
216
|
+
'FILES_LOCAL_ROOT=storage/uploads',
|
|
217
|
+
]);
|
|
218
|
+
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
219
|
+
'FILES_LOCAL_ROOT=storage/uploads',
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildStepBefore,
|
|
6
|
+
ensureBuildSteps,
|
|
7
|
+
ensureClassMember,
|
|
8
|
+
ensureDependency,
|
|
9
|
+
ensureImportLine,
|
|
10
|
+
ensureLineAfter,
|
|
11
|
+
ensureLineBefore,
|
|
12
|
+
ensureLoadItem,
|
|
13
|
+
ensureNestCommonImport,
|
|
14
|
+
ensureValidatorSchema,
|
|
15
|
+
upsertEnvLines,
|
|
16
|
+
} from './shared/patch-utils.mjs';
|
|
17
|
+
|
|
18
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
19
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
|
|
20
|
+
if (!fs.existsSync(source)) {
|
|
21
|
+
throw new Error(`Missing files-quotas preset template: ${source}`);
|
|
22
|
+
}
|
|
23
|
+
const destination = path.join(targetRoot, relativePath);
|
|
24
|
+
copyRecursive(source, destination);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function patchApiPackage(targetRoot) {
|
|
28
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
29
|
+
if (!fs.existsSync(packagePath)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
34
|
+
ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
|
|
35
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-quotas build']);
|
|
36
|
+
ensureBuildStepBefore(
|
|
37
|
+
packageJson,
|
|
38
|
+
'predev',
|
|
39
|
+
'pnpm --filter @forgeon/files-quotas build',
|
|
40
|
+
'pnpm --filter @forgeon/files build',
|
|
41
|
+
);
|
|
42
|
+
writeJson(packagePath, packageJson);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function patchFilesPackage(targetRoot) {
|
|
46
|
+
const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
|
|
47
|
+
if (!fs.existsSync(packagePath)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
52
|
+
ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
|
|
53
|
+
writeJson(packagePath, packageJson);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function patchAppModule(targetRoot) {
|
|
57
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
58
|
+
if (!fs.existsSync(filePath)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
63
|
+
content = ensureImportLine(
|
|
64
|
+
content,
|
|
65
|
+
"import { filesQuotasConfig, filesQuotasEnvSchema, ForgeonFilesQuotasModule } from '@forgeon/files-quotas';",
|
|
66
|
+
);
|
|
67
|
+
content = ensureLoadItem(content, 'filesQuotasConfig');
|
|
68
|
+
content = ensureValidatorSchema(content, 'filesQuotasEnvSchema');
|
|
69
|
+
|
|
70
|
+
if (!content.includes(' ForgeonFilesQuotasModule,')) {
|
|
71
|
+
if (content.includes(' ForgeonFilesAccessModule,')) {
|
|
72
|
+
content = ensureLineAfter(content, ' ForgeonFilesAccessModule,', ' ForgeonFilesQuotasModule,');
|
|
73
|
+
} else if (content.includes(' ForgeonFilesModule,')) {
|
|
74
|
+
content = ensureLineAfter(content, ' ForgeonFilesModule,', ' ForgeonFilesQuotasModule,');
|
|
75
|
+
} else if (content.includes(' ForgeonI18nModule.register({')) {
|
|
76
|
+
content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesQuotasModule,');
|
|
77
|
+
} else if (content.includes(' ForgeonAuthModule.register({')) {
|
|
78
|
+
content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonFilesQuotasModule,');
|
|
79
|
+
} else if (content.includes(' ForgeonAuthModule.register(),')) {
|
|
80
|
+
content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonFilesQuotasModule,');
|
|
81
|
+
} else if (content.includes(' DbPrismaModule,')) {
|
|
82
|
+
content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesQuotasModule,');
|
|
83
|
+
} else if (content.includes(' ForgeonLoggerModule,')) {
|
|
84
|
+
content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonFilesQuotasModule,');
|
|
85
|
+
} else if (content.includes(' ForgeonSwaggerModule,')) {
|
|
86
|
+
content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonFilesQuotasModule,');
|
|
87
|
+
} else {
|
|
88
|
+
content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonFilesQuotasModule,');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function patchFilesController(targetRoot) {
|
|
96
|
+
const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'files.controller.ts');
|
|
97
|
+
if (!fs.existsSync(filePath)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
102
|
+
content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
|
|
103
|
+
|
|
104
|
+
if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
|
|
105
|
+
const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
|
|
106
|
+
if (constructorMatch) {
|
|
107
|
+
const original = constructorMatch[0];
|
|
108
|
+
const inner = constructorMatch[1].trimEnd();
|
|
109
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
110
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
111
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
112
|
+
private readonly filesQuotasService: FilesQuotasService,
|
|
113
|
+
) {`;
|
|
114
|
+
content = content.replace(original, next);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!content.includes('filesQuotasService.assertUploadAllowed')) {
|
|
119
|
+
content = content.replace(
|
|
120
|
+
' return this.filesService.create({',
|
|
121
|
+
` await this.filesQuotasService.assertUploadAllowed({
|
|
122
|
+
ownerType: body.ownerType ?? 'system',
|
|
123
|
+
ownerId: body.ownerId ?? null,
|
|
124
|
+
fileSize: file.size,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return this.filesService.create({`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function patchHealthController(targetRoot) {
|
|
135
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
136
|
+
if (!fs.existsSync(filePath)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
141
|
+
content = ensureNestCommonImport(content, 'Query');
|
|
142
|
+
content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
|
|
143
|
+
|
|
144
|
+
if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
|
|
145
|
+
const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
|
|
146
|
+
if (constructorMatch) {
|
|
147
|
+
const original = constructorMatch[0];
|
|
148
|
+
const inner = constructorMatch[1].trimEnd();
|
|
149
|
+
const normalizedInner = inner.replace(/,\s*$/, '');
|
|
150
|
+
const separator = normalizedInner.length > 0 ? ',' : '';
|
|
151
|
+
const next = `constructor(${normalizedInner}${separator}
|
|
152
|
+
private readonly filesQuotasService: FilesQuotasService,
|
|
153
|
+
) {`;
|
|
154
|
+
content = content.replace(original, next);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!content.includes("@Get('files-quotas')")) {
|
|
159
|
+
const method = `
|
|
160
|
+
@Get('files-quotas')
|
|
161
|
+
async getFilesQuotasProbe(
|
|
162
|
+
@Query('ownerType') ownerType = 'user',
|
|
163
|
+
@Query('ownerId') ownerId = 'probe-owner',
|
|
164
|
+
@Query('size') size = '1024',
|
|
165
|
+
) {
|
|
166
|
+
const parsedSize = Number.isFinite(Number(size)) ? Math.max(1, Number(size)) : 1024;
|
|
167
|
+
return this.filesQuotasService.getProbeStatus({
|
|
168
|
+
ownerType,
|
|
169
|
+
ownerId: ownerId || null,
|
|
170
|
+
fileSize: parsedSize,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function patchWebApp(targetRoot) {
|
|
181
|
+
const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
182
|
+
if (!fs.existsSync(filePath)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
187
|
+
content = content
|
|
188
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
|
|
189
|
+
.replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
|
|
190
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
|
|
191
|
+
.replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
|
|
192
|
+
|
|
193
|
+
if (!content.includes('filesQuotasProbeResult')) {
|
|
194
|
+
const stateAnchors = [
|
|
195
|
+
' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
|
|
196
|
+
' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
|
|
197
|
+
' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
|
|
198
|
+
' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
|
|
199
|
+
' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
|
|
200
|
+
' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
|
|
201
|
+
' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
|
|
202
|
+
];
|
|
203
|
+
const stateAnchor = stateAnchors.find((line) => content.includes(line));
|
|
204
|
+
if (stateAnchor) {
|
|
205
|
+
content = ensureLineAfter(
|
|
206
|
+
content,
|
|
207
|
+
stateAnchor,
|
|
208
|
+
' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!content.includes('Check files quotas')) {
|
|
214
|
+
const probePath = content.includes("runProbe(setHealthResult, '/health')")
|
|
215
|
+
? '/health/files-quotas'
|
|
216
|
+
: '/api/health/files-quotas';
|
|
217
|
+
const button = ` <button onClick={() => runProbe(setFilesQuotasProbeResult, '${probePath}')}>
|
|
218
|
+
Check files quotas
|
|
219
|
+
</button>`;
|
|
220
|
+
|
|
221
|
+
const actionsStart = content.indexOf('<div className="actions">');
|
|
222
|
+
if (actionsStart >= 0) {
|
|
223
|
+
const actionsEnd = content.indexOf('\n </div>', actionsStart);
|
|
224
|
+
if (actionsEnd >= 0) {
|
|
225
|
+
content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!content.includes("{renderResult('Files quotas probe response', filesQuotasProbeResult)}")) {
|
|
231
|
+
const resultLine = " {renderResult('Files quotas probe response', filesQuotasProbeResult)}";
|
|
232
|
+
const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
|
|
233
|
+
if (content.includes(networkLine)) {
|
|
234
|
+
content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
|
|
235
|
+
} else {
|
|
236
|
+
const anchors = [
|
|
237
|
+
" {renderResult('Files access probe response', filesAccessProbeResult)}",
|
|
238
|
+
" {renderResult('Files probe response', filesProbeResult)}",
|
|
239
|
+
" {renderResult('RBAC probe response', rbacProbeResult)}",
|
|
240
|
+
" {renderResult('Rate limit probe response', rateLimitProbeResult)}",
|
|
241
|
+
" {renderResult('Auth probe response', authProbeResult)}",
|
|
242
|
+
" {renderResult('DB probe response', dbProbeResult)}",
|
|
243
|
+
" {renderResult('Validation probe response', validationProbeResult)}",
|
|
244
|
+
];
|
|
245
|
+
const anchor = anchors.find((line) => content.includes(line));
|
|
246
|
+
if (anchor) {
|
|
247
|
+
content = ensureLineAfter(content, anchor, resultLine);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function patchApiDockerfile(targetRoot) {
|
|
256
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
257
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
262
|
+
const packageAnchors = [
|
|
263
|
+
'COPY packages/files-access/package.json packages/files-access/package.json',
|
|
264
|
+
'COPY packages/files/package.json packages/files/package.json',
|
|
265
|
+
'COPY packages/files-local/package.json packages/files-local/package.json',
|
|
266
|
+
'COPY packages/files-s3/package.json packages/files-s3/package.json',
|
|
267
|
+
'COPY packages/auth-api/package.json packages/auth-api/package.json',
|
|
268
|
+
'COPY packages/rbac/package.json packages/rbac/package.json',
|
|
269
|
+
'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
|
|
270
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
271
|
+
'COPY packages/swagger/package.json packages/swagger/package.json',
|
|
272
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
273
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
274
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
275
|
+
];
|
|
276
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
277
|
+
content = ensureLineAfter(
|
|
278
|
+
content,
|
|
279
|
+
packageAnchor,
|
|
280
|
+
'COPY packages/files-quotas/package.json packages/files-quotas/package.json',
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const sourceAnchors = [
|
|
284
|
+
'COPY packages/files-access packages/files-access',
|
|
285
|
+
'COPY packages/files packages/files',
|
|
286
|
+
'COPY packages/files-local packages/files-local',
|
|
287
|
+
'COPY packages/files-s3 packages/files-s3',
|
|
288
|
+
'COPY packages/auth-api packages/auth-api',
|
|
289
|
+
'COPY packages/rbac packages/rbac',
|
|
290
|
+
'COPY packages/rate-limit packages/rate-limit',
|
|
291
|
+
'COPY packages/logger packages/logger',
|
|
292
|
+
'COPY packages/swagger packages/swagger',
|
|
293
|
+
'COPY packages/i18n packages/i18n',
|
|
294
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
295
|
+
'COPY packages/core packages/core',
|
|
296
|
+
];
|
|
297
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
298
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-quotas packages/files-quotas');
|
|
299
|
+
|
|
300
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/files-quotas build\r?\n?/gm, '');
|
|
301
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
|
|
302
|
+
? 'RUN pnpm --filter @forgeon/files build'
|
|
303
|
+
: content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
304
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
305
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
306
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-quotas build');
|
|
307
|
+
|
|
308
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function patchCompose(targetRoot) {
|
|
312
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
313
|
+
if (!fs.existsSync(composePath)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
|
|
318
|
+
if (!content.includes('FILES_QUOTAS_ENABLED: ${FILES_QUOTAS_ENABLED}')) {
|
|
319
|
+
const anchors = [
|
|
320
|
+
/^(\s+FILES_ALLOWED_MIME_PREFIXES:.*)$/m,
|
|
321
|
+
/^(\s+FILES_MAX_FILE_SIZE_BYTES:.*)$/m,
|
|
322
|
+
/^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
|
|
323
|
+
/^(\s+FILES_STORAGE_DRIVER:.*)$/m,
|
|
324
|
+
/^(\s+FILES_ENABLED:.*)$/m,
|
|
325
|
+
/^(\s+API_PREFIX:.*)$/m,
|
|
326
|
+
];
|
|
327
|
+
const anchorPattern = anchors.find((pattern) => pattern.test(content)) ?? anchors.at(-1);
|
|
328
|
+
content = content.replace(
|
|
329
|
+
anchorPattern,
|
|
330
|
+
`$1
|
|
331
|
+
FILES_QUOTAS_ENABLED: \${FILES_QUOTAS_ENABLED}
|
|
332
|
+
FILES_QUOTA_MAX_FILES_PER_OWNER: \${FILES_QUOTA_MAX_FILES_PER_OWNER}
|
|
333
|
+
FILES_QUOTA_MAX_BYTES_PER_OWNER: \${FILES_QUOTA_MAX_BYTES_PER_OWNER}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function patchReadme(targetRoot) {
|
|
341
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
342
|
+
if (!fs.existsSync(readmePath)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const marker = '## Files Quotas Module';
|
|
347
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
348
|
+
if (content.includes(marker)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const section = `## Files Quotas Module
|
|
353
|
+
|
|
354
|
+
The files-quotas module adds owner-based limits for file upload attempts.
|
|
355
|
+
|
|
356
|
+
What it adds:
|
|
357
|
+
- \`@forgeon/files-quotas\` package
|
|
358
|
+
- upload pre-check in files controller
|
|
359
|
+
- probe endpoint: \`GET /api/health/files-quotas\`
|
|
360
|
+
|
|
361
|
+
Current quota model:
|
|
362
|
+
- max files per owner
|
|
363
|
+
- max total bytes per owner
|
|
364
|
+
- owner identity from file payload (\`ownerType\`, \`ownerId\`)
|
|
365
|
+
|
|
366
|
+
Key env:
|
|
367
|
+
- \`FILES_QUOTAS_ENABLED=true\`
|
|
368
|
+
- \`FILES_QUOTA_MAX_FILES_PER_OWNER=100\`
|
|
369
|
+
- \`FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600\``;
|
|
370
|
+
|
|
371
|
+
if (content.includes('## Prisma In Docker Start')) {
|
|
372
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
373
|
+
} else {
|
|
374
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
|
|
381
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
|
|
382
|
+
patchApiPackage(targetRoot);
|
|
383
|
+
patchFilesPackage(targetRoot);
|
|
384
|
+
patchAppModule(targetRoot);
|
|
385
|
+
patchFilesController(targetRoot);
|
|
386
|
+
patchHealthController(targetRoot);
|
|
387
|
+
patchWebApp(targetRoot);
|
|
388
|
+
patchApiDockerfile(targetRoot);
|
|
389
|
+
patchCompose(targetRoot);
|
|
390
|
+
patchReadme(targetRoot);
|
|
391
|
+
|
|
392
|
+
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
393
|
+
'FILES_QUOTAS_ENABLED=true',
|
|
394
|
+
'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
|
|
395
|
+
'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
|
|
396
|
+
]);
|
|
397
|
+
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
398
|
+
'FILES_QUOTAS_ENABLED=true',
|
|
399
|
+
'FILES_QUOTA_MAX_FILES_PER_OWNER=100',
|
|
400
|
+
'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
|
|
401
|
+
]);
|
|
402
|
+
}
|