@directus/api 14.0.1 → 14.1.0
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/dist/__utils__/mock-env.d.ts +18 -0
- package/dist/__utils__/mock-env.js +41 -0
- package/dist/auth/drivers/oauth2.js +12 -5
- package/dist/auth/drivers/openid.js +11 -5
- package/dist/cli/load-extensions.js +1 -2
- package/dist/controllers/assets.js +10 -10
- package/dist/controllers/files.js +1 -5
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/run.js +2 -2
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +5 -0
- package/dist/database/system-data/collections/collections.yaml +1 -1
- package/dist/database/system-data/fields/settings.yaml +12 -13
- package/dist/database/system-data/fields/users.yaml +10 -10
- package/dist/env.d.ts +2 -4
- package/dist/env.js +12 -9
- package/dist/extensions/lib/get-extensions-path.d.ts +1 -0
- package/dist/extensions/lib/get-extensions-path.js +8 -0
- package/dist/extensions/lib/get-extensions.js +3 -2
- package/dist/extensions/lib/sync-extensions.d.ts +1 -0
- package/dist/extensions/lib/sync-extensions.js +59 -0
- package/dist/extensions/lib/sync-status.d.ts +10 -0
- package/dist/extensions/lib/sync-status.js +27 -0
- package/dist/extensions/manager.js +16 -6
- package/dist/logger.d.ts +2 -1
- package/dist/logger.js +13 -2
- package/dist/messenger.js +1 -3
- package/dist/middleware/validate-batch.js +2 -0
- package/dist/request/validate-ip.js +1 -2
- package/dist/services/extensions.js +1 -1
- package/dist/services/files.d.ts +2 -2
- package/dist/services/files.js +93 -26
- package/dist/services/mail/index.js +5 -4
- package/dist/services/mail/templates/base.liquid +383 -138
- package/dist/services/mail/templates/password-reset.liquid +35 -17
- package/dist/services/mail/templates/user-invitation.liquid +32 -13
- package/dist/services/server.js +4 -0
- package/dist/storage/register-drivers.js +1 -2
- package/dist/storage/register-locations.js +1 -2
- package/dist/utils/delete-from-require-cache.js +8 -1
- package/dist/utils/get-auth-providers.d.ts +1 -1
- package/dist/utils/get-config-from-env.js +1 -2
- package/dist/utils/merge-permissions.js +11 -19
- package/dist/utils/sanitize-query.js +2 -2
- package/dist/utils/should-clear-cache.js +1 -2
- package/dist/utils/should-skip-cache.js +3 -4
- package/dist/utils/validate-env.js +1 -2
- package/dist/utils/validate-storage.js +12 -9
- package/package.json +16 -15
- package/dist/__mocks__/cache.d.mts +0 -5
- package/dist/__mocks__/cache.mjs +0 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
|
|
2
2
|
import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES, NESTED_EXTENSION_TYPES } from '@directus/extensions';
|
|
3
|
-
import {
|
|
3
|
+
import { generateExtensionsEntrypoint } from '@directus/extensions/node';
|
|
4
4
|
import { isIn, isTypeIn, pluralize } from '@directus/utils';
|
|
5
5
|
import { pathToRelativeUrl } from '@directus/utils/node';
|
|
6
6
|
import aliasDefault from '@rollup/plugin-alias';
|
|
@@ -27,11 +27,13 @@ import { getSchema } from '../utils/get-schema.js';
|
|
|
27
27
|
import { importFileUrl } from '../utils/import-file-url.js';
|
|
28
28
|
import { JobQueue } from '../utils/job-queue.js';
|
|
29
29
|
import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
|
|
30
|
+
import { getExtensionsPath } from './lib/get-extensions-path.js';
|
|
30
31
|
import { getExtensionsSettings } from './lib/get-extensions-settings.js';
|
|
31
32
|
import { getExtensions } from './lib/get-extensions.js';
|
|
32
33
|
import { getSharedDepsMapping } from './lib/get-shared-deps-mapping.js';
|
|
33
34
|
import { generateApiExtensionsSandboxEntrypoint } from './lib/sandbox/generate-api-extensions-sandbox-entrypoint.js';
|
|
34
35
|
import { instantiateSandboxSdk } from './lib/sandbox/sdk/instantiate.js';
|
|
36
|
+
import { syncExtensions } from './lib/sync-extensions.js';
|
|
35
37
|
import { wrapEmbeds } from './lib/wrap-embeds.js';
|
|
36
38
|
// Workaround for https://github.com/rollup/plugins/issues/1329
|
|
37
39
|
const virtual = virtualDefault;
|
|
@@ -132,13 +134,20 @@ export class ExtensionManager {
|
|
|
132
134
|
*/
|
|
133
135
|
async load() {
|
|
134
136
|
try {
|
|
135
|
-
await
|
|
137
|
+
await syncExtensions();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error(`Failed to sync extensions`);
|
|
141
|
+
logger.error(error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
136
145
|
this.extensions = await getExtensions();
|
|
137
146
|
this.extensionsSettings = await getExtensionsSettings(this.extensions);
|
|
138
147
|
}
|
|
139
|
-
catch (
|
|
148
|
+
catch (error) {
|
|
140
149
|
logger.warn(`Couldn't load extensions`);
|
|
141
|
-
logger.warn(
|
|
150
|
+
logger.warn(error);
|
|
142
151
|
}
|
|
143
152
|
await this.registerHooks();
|
|
144
153
|
await this.registerEndpoints();
|
|
@@ -223,7 +232,7 @@ export class ExtensionManager {
|
|
|
223
232
|
*/
|
|
224
233
|
initializeWatcher() {
|
|
225
234
|
logger.info('Watching extensions for changes...');
|
|
226
|
-
const extensionDirUrl = pathToRelativeUrl(
|
|
235
|
+
const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
|
|
227
236
|
const localExtensionUrls = NESTED_EXTENSION_TYPES.flatMap((type) => {
|
|
228
237
|
const typeDir = path.posix.join(extensionDirUrl, pluralize(type));
|
|
229
238
|
if (isIn(type, HYBRID_EXTENSION_TYPES)) {
|
|
@@ -566,7 +575,8 @@ export class ExtensionManager {
|
|
|
566
575
|
*/
|
|
567
576
|
registerEndpoint(config, name) {
|
|
568
577
|
const endpointRegistrationCallback = typeof config === 'function' ? config : config.handler;
|
|
569
|
-
const
|
|
578
|
+
const nameWithoutType = name.includes(':') ? name.split(':')[0] : name;
|
|
579
|
+
const routeName = typeof config === 'function' ? nameWithoutType : config.id;
|
|
570
580
|
const scopedRouter = express.Router();
|
|
571
581
|
this.endpointRouter.use(`/${routeName}`, scopedRouter);
|
|
572
582
|
endpointRegistrationCallback(scopedRouter, {
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/// <reference types="qs" />
|
|
2
2
|
import type { RequestHandler } from 'express';
|
|
3
|
-
import type
|
|
3
|
+
import { type LoggerOptions } from 'pino';
|
|
4
4
|
export declare const httpLoggerOptions: LoggerOptions;
|
|
5
5
|
declare const logger: import("pino").Logger<LoggerOptions & Record<string, any>>;
|
|
6
|
+
export declare const httpLoggerEnvConfig: Record<string, any>;
|
|
6
7
|
export declare const expressLogger: RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
7
8
|
export default logger;
|
package/dist/logger.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { REDACTED_TEXT, toArray } from '@directus/utils';
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
|
+
import { URL } from 'node:url';
|
|
3
4
|
import { pino } from 'pino';
|
|
4
5
|
import { pinoHttp, stdSerializers } from 'pino-http';
|
|
5
|
-
import { URL } from 'url';
|
|
6
6
|
import env from './env.js';
|
|
7
7
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
8
8
|
const pinoOptions = {
|
|
@@ -82,7 +82,18 @@ if (loggerEnvConfig['levels']) {
|
|
|
82
82
|
delete loggerEnvConfig['levels'];
|
|
83
83
|
}
|
|
84
84
|
const logger = pino(merge(pinoOptions, loggerEnvConfig));
|
|
85
|
-
const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
|
|
85
|
+
export const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
|
|
86
|
+
if (env['LOG_HTTP_IGNORE_PATHS']) {
|
|
87
|
+
const ignorePathsSet = new Set(env['LOG_HTTP_IGNORE_PATHS']);
|
|
88
|
+
httpLoggerEnvConfig['autoLogging'] = {
|
|
89
|
+
ignore: (req) => {
|
|
90
|
+
if (!req.url)
|
|
91
|
+
return false;
|
|
92
|
+
const { pathname } = new URL(req.url, 'http://example.com/');
|
|
93
|
+
return ignorePathsSet.has(pathname);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
86
97
|
export const expressLogger = pinoHttp({
|
|
87
98
|
logger: pino(merge(httpLoggerOptions, loggerEnvConfig)),
|
|
88
99
|
...httpLoggerEnvConfig,
|
package/dist/messenger.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseJSON } from '@directus/utils';
|
|
2
2
|
import { Redis } from 'ioredis';
|
|
3
|
-
import
|
|
3
|
+
import env from './env.js';
|
|
4
4
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
5
5
|
export class MessengerMemory {
|
|
6
6
|
handlers;
|
|
@@ -30,7 +30,6 @@ export class MessengerRedis {
|
|
|
30
30
|
sub;
|
|
31
31
|
constructor() {
|
|
32
32
|
const config = getConfigFromEnv('REDIS');
|
|
33
|
-
const env = getEnv();
|
|
34
33
|
this.pub = new Redis(env['REDIS'] ?? config);
|
|
35
34
|
this.sub = new Redis(env['REDIS'] ?? config);
|
|
36
35
|
this.namespace = env['MESSENGER_NAMESPACE'] ?? 'directus-messenger';
|
|
@@ -55,7 +54,6 @@ let messenger;
|
|
|
55
54
|
export function getMessenger() {
|
|
56
55
|
if (messenger)
|
|
57
56
|
return messenger;
|
|
58
|
-
const env = getEnv();
|
|
59
57
|
if (env['MESSENGER_STORE'] === 'redis') {
|
|
60
58
|
messenger = new MessengerRedis();
|
|
61
59
|
}
|
|
@@ -2,6 +2,7 @@ import Joi from 'joi';
|
|
|
2
2
|
import { InvalidPayloadError } from '@directus/errors';
|
|
3
3
|
import asyncHandler from '../utils/async-handler.js';
|
|
4
4
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
5
|
+
import { validateQuery } from '../utils/validate-query.js';
|
|
5
6
|
export const validateBatch = (scope) => asyncHandler(async (req, _res, next) => {
|
|
6
7
|
if (req.method.toLowerCase() === 'get') {
|
|
7
8
|
req.body = {};
|
|
@@ -18,6 +19,7 @@ export const validateBatch = (scope) => asyncHandler(async (req, _res, next) =>
|
|
|
18
19
|
// In reads, the query in the body should override the query params for searching
|
|
19
20
|
if (scope === 'read' && req.body.query) {
|
|
20
21
|
req.sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
22
|
+
validateQuery(req.sanitizedQuery);
|
|
21
23
|
}
|
|
22
24
|
// Every cRUD action has either keys or query
|
|
23
25
|
let batchSchema = Joi.object().keys({
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import
|
|
2
|
+
import env from '../env.js';
|
|
3
3
|
export const validateIP = async (ip, url) => {
|
|
4
|
-
const env = getEnv();
|
|
5
4
|
if (env['IMPORT_IP_DENY_LIST'].includes(ip)) {
|
|
6
5
|
throw new Error(`Requested URL "${url}" resolves to a denied IP address`);
|
|
7
6
|
}
|
|
@@ -128,7 +128,7 @@ export class ExtensionsService {
|
|
|
128
128
|
return {
|
|
129
129
|
name,
|
|
130
130
|
bundle: bundleName,
|
|
131
|
-
schema: schema ? pick(schema, 'type', 'local') : null,
|
|
131
|
+
schema: schema ? pick(schema, 'type', 'local', 'version') : null,
|
|
132
132
|
meta: omit(meta, 'name'),
|
|
133
133
|
};
|
|
134
134
|
});
|
package/dist/services/files.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
import type { File } from '@directus/types';
|
|
2
|
+
import type { File, BusboyFileStream } from '@directus/types';
|
|
3
3
|
import type { Readable } from 'node:stream';
|
|
4
4
|
import type { AbstractServiceOptions, MutationOptions, PrimaryKey } from '../types/index.js';
|
|
5
5
|
import { ItemsService } from './items.js';
|
|
@@ -9,7 +9,7 @@ export declare class FilesService extends ItemsService {
|
|
|
9
9
|
/**
|
|
10
10
|
* Upload a single new file to the configured storage adapter
|
|
11
11
|
*/
|
|
12
|
-
uploadOne(stream: Readable, data: Partial<File> & {
|
|
12
|
+
uploadOne(stream: BusboyFileStream | Readable, data: Partial<File> & {
|
|
13
13
|
storage: string;
|
|
14
14
|
}, primaryKey?: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
15
15
|
/**
|
package/dist/services/files.js
CHANGED
|
@@ -14,7 +14,7 @@ import url from 'url';
|
|
|
14
14
|
import { SUPPORTED_IMAGE_METADATA_FORMATS } from '../constants.js';
|
|
15
15
|
import emitter from '../emitter.js';
|
|
16
16
|
import env from '../env.js';
|
|
17
|
-
import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
|
|
17
|
+
import { ContentTooLargeError, ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
|
|
18
18
|
import logger from '../logger.js';
|
|
19
19
|
import { getAxios } from '../request/index.js';
|
|
20
20
|
import { getStorage } from '../storage/index.js';
|
|
@@ -29,59 +29,125 @@ export class FilesService extends ItemsService {
|
|
|
29
29
|
*/
|
|
30
30
|
async uploadOne(stream, data, primaryKey, opts) {
|
|
31
31
|
const storage = await getStorage();
|
|
32
|
-
let existingFile =
|
|
32
|
+
let existingFile = null;
|
|
33
|
+
// If the payload contains a primary key, we'll check if the file already exists
|
|
33
34
|
if (primaryKey !== undefined) {
|
|
35
|
+
// If the file you're uploading already exists, we'll consider this upload a replace so we'll fetch the existing file's folder and filename_download
|
|
34
36
|
existingFile =
|
|
35
37
|
(await this.knex
|
|
36
|
-
.select('folder', 'filename_download')
|
|
38
|
+
.select('folder', 'filename_download', 'filename_disk', 'title', 'description', 'metadata')
|
|
37
39
|
.from('directus_files')
|
|
38
40
|
.where({ id: primaryKey })
|
|
39
|
-
.first()) ??
|
|
41
|
+
.first()) ?? null;
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
// Merge the existing file's folder and filename_download with the new payload
|
|
44
|
+
const payload = { ...(existingFile ?? {}), ...clone(data) };
|
|
45
|
+
const disk = storage.location(payload.storage);
|
|
46
|
+
// If no folder is specified, we'll use the default folder from the settings if it exists
|
|
42
47
|
if ('folder' in payload === false) {
|
|
43
48
|
const settings = await this.knex.select('storage_default_folder').from('directus_settings').first();
|
|
44
49
|
if (settings?.storage_default_folder) {
|
|
45
50
|
payload.folder = settings.storage_default_folder;
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const disk = storage.location(payload.storage);
|
|
53
|
-
for await (const filepath of disk.list(String(primaryKey))) {
|
|
54
|
-
await disk.delete(filepath);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
53
|
+
// Is this file a replacement? if the file data already exists and we have a primary key
|
|
54
|
+
const isReplacement = existingFile !== null && primaryKey !== undefined;
|
|
55
|
+
// If this is a new file upload, we need to generate a new primary key and DB record
|
|
56
|
+
if (isReplacement === false || primaryKey === undefined) {
|
|
58
57
|
primaryKey = await this.createOne(payload, { emitEvents: false });
|
|
59
58
|
}
|
|
60
59
|
const fileExtension = path.extname(payload.filename_download) || (payload.type && '.' + extension(payload.type)) || '';
|
|
60
|
+
// The filename_disk is the FINAL filename on disk
|
|
61
61
|
payload.filename_disk = primaryKey + (fileExtension || '');
|
|
62
|
+
// Temp filename is used for replacements
|
|
63
|
+
const tempFilenameDisk = 'temp_' + payload.filename_disk;
|
|
62
64
|
if (!payload.type) {
|
|
63
65
|
payload.type = 'application/octet-stream';
|
|
64
66
|
}
|
|
67
|
+
// Used to clean up if something goes wrong
|
|
68
|
+
const cleanUp = async () => {
|
|
69
|
+
try {
|
|
70
|
+
if (isReplacement === true) {
|
|
71
|
+
// If this is a replacement that failed, we need to delete the temp file
|
|
72
|
+
await disk.delete(tempFilenameDisk);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// If this is a new file that failed
|
|
76
|
+
// delete the DB record
|
|
77
|
+
await super.deleteMany([primaryKey]);
|
|
78
|
+
// delete the final file
|
|
79
|
+
await disk.delete(payload.filename_disk);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (isReplacement === true) {
|
|
84
|
+
logger.warn(`Couldn't delete temp file ${tempFilenameDisk}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.warn(`Couldn't delete file ${payload.filename_disk}`);
|
|
88
|
+
}
|
|
89
|
+
logger.warn(err);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
65
92
|
try {
|
|
66
|
-
|
|
93
|
+
// If this is a replacement, we'll write the file to a temp location first to ensure we don't overwrite the existing file if something goes wrong
|
|
94
|
+
if (isReplacement === true) {
|
|
95
|
+
await disk.write(tempFilenameDisk, stream, payload.type);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// If this is a new file upload, we'll write the file to the final location
|
|
99
|
+
await disk.write(payload.filename_disk, stream, payload.type);
|
|
100
|
+
}
|
|
101
|
+
// Check if the file was truncated (if the stream ended early) and throw limit error if it was
|
|
102
|
+
if ('truncated' in stream && stream.truncated === true) {
|
|
103
|
+
throw new ContentTooLargeError();
|
|
104
|
+
}
|
|
67
105
|
}
|
|
68
106
|
catch (err) {
|
|
69
107
|
logger.warn(`Couldn't save file ${payload.filename_disk}`);
|
|
70
108
|
logger.warn(err);
|
|
71
|
-
await
|
|
72
|
-
|
|
109
|
+
await cleanUp();
|
|
110
|
+
if (err instanceof ContentTooLargeError) {
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new ServiceUnavailableError({ service: 'files', reason: `Couldn't save file ${payload.filename_disk}` });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// If the file is a replacement, we need to update the DB record with the new payload, delete the old files, and upgrade the temp file
|
|
118
|
+
if (isReplacement === true) {
|
|
119
|
+
await this.updateOne(primaryKey, payload, { emitEvents: false });
|
|
120
|
+
// delete the previously saved file and thumbnails to ensure they're generated fresh
|
|
121
|
+
for await (const filepath of disk.list(String(primaryKey))) {
|
|
122
|
+
await disk.delete(filepath);
|
|
123
|
+
}
|
|
124
|
+
// Upgrade the temp file to the final filename
|
|
125
|
+
await disk.move(tempFilenameDisk, payload.filename_disk);
|
|
73
126
|
}
|
|
74
127
|
const { size } = await storage.location(data.storage).stat(payload.filename_disk);
|
|
75
128
|
payload.filesize = size;
|
|
76
129
|
if (SUPPORTED_IMAGE_METADATA_FORMATS.includes(payload.type)) {
|
|
77
130
|
const stream = await storage.location(data.storage).read(payload.filename_disk);
|
|
78
131
|
const { height, width, description, title, tags, metadata } = await this.getMetadata(stream);
|
|
79
|
-
payload.height
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
payload.
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
if (!payload.height && height) {
|
|
133
|
+
payload.height = height;
|
|
134
|
+
}
|
|
135
|
+
if (!payload.width && width) {
|
|
136
|
+
payload.width = width;
|
|
137
|
+
}
|
|
138
|
+
if (!payload.metadata && metadata) {
|
|
139
|
+
payload.metadata = metadata;
|
|
140
|
+
}
|
|
141
|
+
// Note that if this is a replace file upload, the below properities are fetched and included in the payload above in the `existingFile` variable...so this will ONLY set the values if they're not already set
|
|
142
|
+
if (!payload.description && description) {
|
|
143
|
+
payload.description = description;
|
|
144
|
+
}
|
|
145
|
+
if (!payload.title && title) {
|
|
146
|
+
payload.title = title;
|
|
147
|
+
}
|
|
148
|
+
if (!payload.tags && tags) {
|
|
149
|
+
payload.tags = tags;
|
|
150
|
+
}
|
|
85
151
|
}
|
|
86
152
|
// We do this in a service without accountability. Even if you don't have update permissions to the file,
|
|
87
153
|
// we still want to be able to set the extracted values from the file on create
|
|
@@ -255,15 +321,16 @@ export class FilesService extends ItemsService {
|
|
|
255
321
|
*/
|
|
256
322
|
async deleteMany(keys) {
|
|
257
323
|
const storage = await getStorage();
|
|
258
|
-
const files = await super.readMany(keys, { fields: ['id', 'storage'], limit: -1 });
|
|
324
|
+
const files = await super.readMany(keys, { fields: ['id', 'storage', 'filename_disk'], limit: -1 });
|
|
259
325
|
if (!files) {
|
|
260
326
|
throw new ForbiddenError();
|
|
261
327
|
}
|
|
262
328
|
await super.deleteMany(keys);
|
|
263
329
|
for (const file of files) {
|
|
264
330
|
const disk = storage.location(file['storage']);
|
|
331
|
+
const filePrefix = path.parse(file['filename_disk']).name;
|
|
265
332
|
// Delete file + thumbnails
|
|
266
|
-
for await (const filepath of disk.list(
|
|
333
|
+
for await (const filepath of disk.list(filePrefix)) {
|
|
267
334
|
await disk.delete(filepath);
|
|
268
335
|
}
|
|
269
336
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
1
2
|
import fse from 'fs-extra';
|
|
2
3
|
import { Liquid } from 'liquidjs';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
import getDatabase from '../../database/index.js';
|
|
6
7
|
import env from '../../env.js';
|
|
7
|
-
import {
|
|
8
|
+
import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
|
|
8
9
|
import logger from '../../logger.js';
|
|
9
10
|
import getMailer from '../../mailer.js';
|
|
10
11
|
import { Url } from '../../utils/url.js';
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const liquidEngine = new Liquid({
|
|
13
|
-
root: [path.resolve(
|
|
14
|
+
root: [path.resolve(getExtensionsPath(), 'templates'), path.resolve(__dirname, 'templates')],
|
|
14
15
|
extname: '.liquid',
|
|
15
16
|
});
|
|
16
17
|
export class MailService {
|
|
@@ -56,7 +57,7 @@ export class MailService {
|
|
|
56
57
|
return info;
|
|
57
58
|
}
|
|
58
59
|
async renderTemplate(template, variables) {
|
|
59
|
-
const customTemplatePath = path.resolve(
|
|
60
|
+
const customTemplatePath = path.resolve(getExtensionsPath(), 'templates', template + '.liquid');
|
|
60
61
|
const systemTemplatePath = path.join(__dirname, 'templates', template + '.liquid');
|
|
61
62
|
const templatePath = (await fse.pathExists(customTemplatePath)) ? customTemplatePath : systemTemplatePath;
|
|
62
63
|
if ((await fse.pathExists(templatePath)) === false) {
|
|
@@ -73,7 +74,7 @@ export class MailService {
|
|
|
73
74
|
.first();
|
|
74
75
|
return {
|
|
75
76
|
projectName: projectInfo?.project_name || 'Directus',
|
|
76
|
-
projectColor: projectInfo?.project_color || '#
|
|
77
|
+
projectColor: projectInfo?.project_color || '#171717',
|
|
77
78
|
projectLogo: getProjectLogoURL(projectInfo?.project_logo),
|
|
78
79
|
projectUrl: projectInfo?.project_url || '',
|
|
79
80
|
};
|