Skip to content

Commit 7df84c0

Browse files
rijkvanzantenlicitdevbr41nslugpaescuj
authoredNov 16, 2023
Add support for EXTENSIONS_LOCATION setting (directus#20207)
Co-authored-by: ian <[email protected]> Co-authored-by: Brainslug <[email protected]> Co-authored-by: Pascal Jufer <[email protected]>
1 parent 013b893 commit 7df84c0

File tree

17 files changed

+216
-45
lines changed

17 files changed

+216
-45
lines changed
 

‎.changeset/few-mice-know.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@directus/api': minor
3+
---
4+
5+
Added configuration to allow extensions to be managed in a configured storage location

‎.eslintignore

+2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ dist/
99
*.yml
1010
*.yaml
1111
*.md
12+
*.txt
1213
*.json
1314
*.scss
1415
*.css
1516
*.svg
17+
Dockerfile

‎Dockerfile

-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ EXPOSE 8055
5454
ENV \
5555
DB_CLIENT="sqlite3" \
5656
DB_FILENAME="/directus/database/database.sqlite" \
57-
EXTENSIONS_PATH="/directus/extensions" \
58-
STORAGE_LOCAL_ROOT="/directus/uploads" \
5957
NODE_ENV="production" \
6058
NPM_CONFIG_UPDATE_NOTIFIER="false"
6159

‎api/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"openid-client": "5.4.2",
146146
"ora": "6.3.1",
147147
"otplib": "12.0.1",
148+
"p-queue": "7.4.1",
148149
"papaparse": "5.4.1",
149150
"pino": "8.14.1",
150151
"pino-http": "8.3.3",

‎api/src/database/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from 'path';
1010
import { performance } from 'perf_hooks';
1111
import { promisify } from 'util';
1212
import env from '../env.js';
13+
import { getExtensionsPath } from '../extensions/lib/get-extensions-path.js';
1314
import logger from '../logger.js';
1415
import type { DatabaseClient } from '../types/index.js';
1516
import { getConfigFromEnv } from '../utils/get-config-from-env.js';
@@ -258,7 +259,7 @@ export async function validateMigrations(): Promise<boolean> {
258259
try {
259260
let migrationFiles = await fse.readdir(path.join(__dirname, 'migrations'));
260261

261-
const customMigrationsPath = path.resolve(env['EXTENSIONS_PATH'], 'migrations');
262+
const customMigrationsPath = path.resolve(getExtensionsPath(), 'migrations');
262263

263264
let customMigrationFiles =
264265
((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];

‎api/src/database/migrations/run.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { dirname } from 'node:path';
66
import { fileURLToPath } from 'node:url';
77
import path from 'path';
88
import { flushCaches } from '../../cache.js';
9-
import env from '../../env.js';
9+
import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
1010
import logger from '../../logger.js';
1111
import type { Migration } from '../../types/index.js';
1212
import getModuleDefault from '../../utils/get-module-default.js';
@@ -16,7 +16,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
1616
export default async function run(database: Knex, direction: 'up' | 'down' | 'latest', log = true): Promise<void> {
1717
let migrationFiles = await fse.readdir(__dirname);
1818

19-
const customMigrationsPath = path.resolve(env['EXTENSIONS_PATH'], 'migrations');
19+
const customMigrationsPath = path.resolve(getExtensionsPath(), 'migrations');
2020

2121
let customMigrationFiles =
2222
((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];

‎api/src/env.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* For all possible keys, see: https://docs.directus.io/self-hosted/config-options/
44
*/
55

6-
import { parseJSON, toArray } from '@directus/utils';
76
import { JAVASCRIPT_FILE_EXTS } from '@directus/constants';
7+
import { parseJSON, toArray } from '@directus/utils';
88
import dotenv from 'dotenv';
99
import fs from 'fs';
1010
import { clone, toNumber, toString } from 'lodash-es';
@@ -35,6 +35,7 @@ const allowedEnvironmentVars = [
3535
'QUERY_LIMIT_MAX',
3636
'QUERY_LIMIT_DEFAULT',
3737
'ROBOTS_TXT',
38+
'TEMP_PATH',
3839
// server
3940
'SERVER_.+',
4041
// database
@@ -163,6 +164,7 @@ const allowedEnvironmentVars = [
163164
'AUTH_.+_SP.+',
164165
// extensions
165166
'PACKAGE_FILE_LOCATION',
167+
'EXTENSIONS_LOCATION',
166168
'EXTENSIONS_PATH',
167169
'EXTENSIONS_AUTO_RELOAD',
168170
'EXTENSIONS_CACHE_TTL',
@@ -225,6 +227,8 @@ export const defaults: Record<string, any> = {
225227
MAX_BATCH_MUTATION: Infinity,
226228
ROBOTS_TXT: 'User-agent: *\nDisallow: /',
227229

230+
TEMP_PATH: './node_modules/.directus',
231+
228232
DB_EXCLUDE_TABLES: 'spatial_ref_sys,sysdiagrams',
229233

230234
STORAGE_LOCATIONS: 'local',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { join } from 'path';
2+
import env from '../../env.js';
3+
4+
export const getExtensionsPath = () => {
5+
if (env['EXTENSIONS_LOCATION']) {
6+
return join(env['TEMP_PATH'], 'extensions');
7+
}
8+
9+
return env['EXTENSIONS_PATH'];
10+
};

‎api/src/extensions/lib/get-extensions.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import type { Extension } from '@directus/extensions';
22
import { getLocalExtensions, getPackageExtensions, resolvePackageExtensions } from '@directus/extensions/node';
33
import env from '../../env.js';
4+
import { getExtensionsPath } from './get-extensions-path.js';
45

56
export const getExtensions = async () => {
6-
const localExtensions = await getLocalExtensions(env['EXTENSIONS_PATH']);
7+
const localExtensions = await getLocalExtensions(getExtensionsPath());
78

89
const loadedNames = localExtensions.map(({ name }) => name);
910

1011
const filterDuplicates = ({ name }: Extension) => loadedNames.includes(name) === false;
1112

12-
const localPackageExtensions = (await resolvePackageExtensions(env['EXTENSIONS_PATH'])).filter((extension) =>
13+
const localPackageExtensions = (await resolvePackageExtensions(getExtensionsPath())).filter((extension) =>
1314
filterDuplicates(extension)
1415
);
1516

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { NESTED_EXTENSION_TYPES } from '@directus/extensions';
2+
import { ensureExtensionDirs } from '@directus/extensions/node';
3+
import mid from 'node-machine-id';
4+
import { createWriteStream } from 'node:fs';
5+
import { mkdir } from 'node:fs/promises';
6+
import { dirname, join, relative, resolve, sep } from 'node:path';
7+
import { pipeline } from 'node:stream/promises';
8+
import Queue from 'p-queue';
9+
import env from '../../env.js';
10+
import logger from '../../logger.js';
11+
import { getMessenger } from '../../messenger.js';
12+
import { getStorage } from '../../storage/index.js';
13+
import { getExtensionsPath } from './get-extensions-path.js';
14+
import { SyncStatus, getSyncStatus, setSyncStatus } from './sync-status.js';
15+
16+
export const syncExtensions = async (): Promise<void> => {
17+
const extensionsPath = getExtensionsPath();
18+
19+
if (!env['EXTENSIONS_LOCATION']) {
20+
// Safe to run with multiple instances since dirs are created with `recursive: true`
21+
return ensureExtensionDirs(extensionsPath, NESTED_EXTENSION_TYPES);
22+
}
23+
24+
const messenger = getMessenger();
25+
26+
const isPrimaryProcess =
27+
String(process.env['NODE_APP_INSTANCE']) === '0' || process.env['NODE_APP_INSTANCE'] === undefined;
28+
29+
const id = await mid.machineId();
30+
31+
const message = `extensions-sync/${id}`;
32+
33+
if (isPrimaryProcess === false) {
34+
const isDone = (await getSyncStatus()) === SyncStatus.DONE;
35+
36+
if (isDone) return;
37+
38+
logger.trace('Extensions already being synced to this machine from another process.');
39+
40+
/**
41+
* Wait until the process that called the lock publishes a message that the syncing is complete
42+
*/
43+
return new Promise((resolve) => {
44+
messenger.subscribe(message, () => resolve());
45+
});
46+
}
47+
48+
// Ensure that the local extensions cache path exists
49+
await mkdir(extensionsPath, { recursive: true });
50+
await setSyncStatus(SyncStatus.SYNCING);
51+
52+
logger.trace('Syncing extensions from configured storage location...');
53+
54+
const storage = await getStorage();
55+
56+
const disk = storage.location(env['EXTENSIONS_LOCATION']);
57+
58+
// Make sure we don't overload the file handles
59+
const queue = new Queue({ concurrency: 1000 });
60+
61+
for await (const filepath of disk.list(env['EXTENSIONS_PATH'])) {
62+
const readStream = await disk.read(filepath);
63+
64+
// We want files to be stored in the root of `$TEMP_PATH/extensions`, so gotta remove the
65+
// extensions path on disk from the start of the file path
66+
const destPath = join(extensionsPath, relative(resolve(sep, env['EXTENSIONS_PATH']), resolve(sep, filepath)));
67+
68+
// Ensure that the directory path exists
69+
await mkdir(dirname(destPath), { recursive: true });
70+
71+
const writeStream = createWriteStream(destPath);
72+
73+
queue.add(() => pipeline(readStream, writeStream));
74+
}
75+
76+
await queue.onIdle();
77+
78+
await ensureExtensionDirs(extensionsPath, NESTED_EXTENSION_TYPES);
79+
80+
await setSyncStatus(SyncStatus.DONE);
81+
messenger.publish(message, { ready: true });
82+
};

‎api/src/extensions/lib/sync-status.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { exists } from 'fs-extra';
2+
import { readFile, writeFile } from 'node:fs/promises';
3+
import { join } from 'node:path';
4+
import { getExtensionsPath } from './get-extensions-path.js';
5+
6+
export enum SyncStatus {
7+
UNKNOWN = 'UNKNOWN',
8+
SYNCING = 'SYNCING',
9+
DONE = 'DONE',
10+
}
11+
12+
/**
13+
* Retrieves the sync status from the `.status` file in the local extensions folder
14+
*/
15+
export const getSyncStatus = async () => {
16+
const statusFilePath = join(getExtensionsPath(), '.status');
17+
18+
if (await exists(statusFilePath)) {
19+
const status = await readFile(statusFilePath, 'utf8');
20+
return status;
21+
} else {
22+
return SyncStatus.UNKNOWN;
23+
}
24+
};
25+
26+
export const setSyncStatus = async (status: SyncStatus) => {
27+
const statusFilePath = join(getExtensionsPath(), '.status');
28+
await writeFile(statusFilePath, status);
29+
};

‎api/src/extensions/manager.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
OperationApiConfig,
1111
} from '@directus/extensions';
1212
import { APP_SHARED_DEPS, HYBRID_EXTENSION_TYPES, NESTED_EXTENSION_TYPES } from '@directus/extensions';
13-
import { ensureExtensionDirs, generateExtensionsEntrypoint } from '@directus/extensions/node';
13+
import { generateExtensionsEntrypoint } from '@directus/extensions/node';
1414
import type {
1515
ActionHandler,
1616
EmbedHandler,
@@ -45,11 +45,13 @@ import { getSchema } from '../utils/get-schema.js';
4545
import { importFileUrl } from '../utils/import-file-url.js';
4646
import { JobQueue } from '../utils/job-queue.js';
4747
import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
48+
import { getExtensionsPath } from './lib/get-extensions-path.js';
4849
import { getExtensionsSettings } from './lib/get-extensions-settings.js';
4950
import { getExtensions } from './lib/get-extensions.js';
5051
import { getSharedDepsMapping } from './lib/get-shared-deps-mapping.js';
5152
import { generateApiExtensionsSandboxEntrypoint } from './lib/sandbox/generate-api-extensions-sandbox-entrypoint.js';
5253
import { instantiateSandboxSdk } from './lib/sandbox/sdk/instantiate.js';
54+
import { syncExtensions } from './lib/sync-extensions.js';
5355
import { wrapEmbeds } from './lib/wrap-embeds.js';
5456
import type { BundleConfig, ExtensionManagerOptions } from './types.js';
5557

@@ -173,7 +175,7 @@ export class ExtensionManager {
173175
*/
174176
private async load(): Promise<void> {
175177
try {
176-
await ensureExtensionDirs(env['EXTENSIONS_PATH'], NESTED_EXTENSION_TYPES);
178+
await syncExtensions();
177179

178180
this.extensions = await getExtensions();
179181
this.extensionsSettings = await getExtensionsSettings(this.extensions);
@@ -290,7 +292,7 @@ export class ExtensionManager {
290292
private initializeWatcher(): void {
291293
logger.info('Watching extensions for changes...');
292294

293-
const extensionDirUrl = pathToRelativeUrl(env['EXTENSIONS_PATH']);
295+
const extensionDirUrl = pathToRelativeUrl(getExtensionsPath());
294296

295297
const localExtensionUrls = NESTED_EXTENSION_TYPES.flatMap((type) => {
296298
const typeDir = path.posix.join(extensionDirUrl, pluralize(type));

‎api/src/services/mail/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { InvalidPayloadError } from '@directus/errors';
12
import type { Accountability, SchemaOverview } from '@directus/types';
23
import fse from 'fs-extra';
34
import type { Knex } from 'knex';
@@ -7,7 +8,7 @@ import path from 'path';
78
import { fileURLToPath } from 'url';
89
import getDatabase from '../../database/index.js';
910
import env from '../../env.js';
10-
import { InvalidPayloadError } from '@directus/errors';
11+
import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
1112
import logger from '../../logger.js';
1213
import getMailer from '../../mailer.js';
1314
import type { AbstractServiceOptions } from '../../types/index.js';
@@ -16,7 +17,7 @@ import { Url } from '../../utils/url.js';
1617
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1718

1819
const liquidEngine = new Liquid({
19-
root: [path.resolve(env['EXTENSIONS_PATH'], 'templates'), path.resolve(__dirname, 'templates')],
20+
root: [path.resolve(getExtensionsPath(), 'templates'), path.resolve(__dirname, 'templates')],
2021
extname: '.liquid',
2122
});
2223

@@ -81,7 +82,7 @@ export class MailService {
8182
}
8283

8384
private async renderTemplate(template: string, variables: Record<string, any>) {
84-
const customTemplatePath = path.resolve(env['EXTENSIONS_PATH'], 'templates', template + '.liquid');
85+
const customTemplatePath = path.resolve(getExtensionsPath(), 'templates', template + '.liquid');
8586
const systemTemplatePath = path.join(__dirname, 'templates', template + '.liquid');
8687

8788
const templatePath = (await fse.pathExists(customTemplatePath)) ? customTemplatePath : systemTemplatePath;

‎api/src/utils/validate-storage.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import env from '../env.js';
2-
import logger from '../logger.js';
3-
import { access } from 'node:fs/promises';
1+
import { toArray } from '@directus/utils';
42
import { constants } from 'fs';
3+
import { access } from 'node:fs/promises';
54
import path from 'path';
6-
import { toArray } from '@directus/utils';
5+
import env from '../env.js';
6+
import { getExtensionsPath } from '../extensions/lib/get-extensions-path.js';
7+
import logger from '../logger.js';
78

89
export async function validateStorage(): Promise<void> {
910
if (env['DB_CLIENT'] === 'sqlite3') {
@@ -28,9 +29,11 @@ export async function validateStorage(): Promise<void> {
2829
}
2930
}
3031

31-
try {
32-
await access(env['EXTENSIONS_PATH'], constants.R_OK);
33-
} catch {
34-
logger.warn(`Extensions directory (${path.resolve(env['EXTENSIONS_PATH'])}) is not readable!`);
32+
if (!env['EXTENSIONS_LOCATION']) {
33+
try {
34+
await access(getExtensionsPath(), constants.R_OK);
35+
} catch {
36+
logger.warn(`Extensions directory (${path.resolve(getExtensionsPath())}) is not readable!`);
37+
}
3538
}
3639
}

‎docs/self-hosted/config-options.md

+30-17
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ prefixing the value with `{type}:`. The following types are available:
244244
| `QUERY_LIMIT_DEFAULT` | The default query limit used when not defined in the API request. | `100` |
245245
| `QUERY_LIMIT_MAX` | The maximum query limit accepted on API requests. | `-1` |
246246
| `ROBOTS_TXT` | What the `/robots.txt` endpoint should return | `User-agent: *\nDisallow: /` |
247+
| `TEMP_PATH` | Where Directus' temporary files should be managed | `./node_modules/.directus` |
247248

248249
<sup>[1]</sup> The PUBLIC_URL value is used for things like OAuth redirects, forgot-password emails, and logos that
249250
needs to be publicly available on the internet.
@@ -898,19 +899,29 @@ const publicUrl = process.env.PUBLIC_URL;
898899

899900
## Extensions
900901

901-
| Variable | Description | Default Value |
902-
| ------------------------------------ | ------------------------------------------------------- | -------------- |
903-
| `EXTENSIONS_PATH` | Path to your local extensions folder. | `./extensions` |
904-
| `EXTENSIONS_AUTO_RELOAD` | Automatically reload extensions when they have changed. | `false` |
905-
| `EXTENSIONS_CACHE_TTL`<sup>[1]</sup> | How long custom app Extensions get cached by browsers. | -- |
902+
| Variable | Description | Default Value |
903+
| -------------------------------------- | ------------------------------------------------------- | -------------- |
904+
| `EXTENSIONS_PATH`<sup>[1]</sup> | Path to your local extensions folder. | `./extensions` |
905+
| `EXTENSIONS_AUTO_RELOAD`<sup>[2]</sup> | Automatically reload extensions when they have changed. | `false` |
906+
| `EXTENSIONS_CACHE_TTL`<sup>[3]</sup> | How long custom app Extensions get cached by browsers. | -- |
907+
| `EXTENSIONS_LOCATION`<sup>[4]</sup> | What configured storage location to use for extensions. | -- |
906908

907-
<sup>[1]</sup> The `EXTENSIONS_CACHE_TTL` environment variable controls for how long custom app extensions (e.t.,
909+
<sup>[1]</sup> If `EXTENSIONS_LOCATION` is configured, this is the path to the extensions folder within the selected
910+
storage location.
911+
912+
<sup>[2]</sup> `EXTENSIONS_AUTO_RELOAD` will not work when the `EXTENSION_LOCATION` environment variable is set.
913+
914+
<sup>[3]</sup> The `EXTENSIONS_CACHE_TTL` environment variable controls for how long custom app extensions (e.t.,
908915
interface, display, layout, module, panel) are cached by browsers. Caching can speed-up the loading of the app as the
909916
code for the extensions doesn't need to be re-fetched from the server on each app reload. On the other hand, this means
910917
that code changes to app extensions won't be taken into account by the browser until `EXTENSIONS_CACHE_TTL` has expired.
911918
By default, extensions are not cached. The input data type for this environment variable is the same as
912919
[`CACHE_TTL`](#cache).
913920

921+
<sup>[4]</sup> By default extensions are loaded from the local file system. `EXTENSIONS_LOCATION` can be used to load
922+
extensions from a storage location instead. Under the hood, they are synced into a local directory within `TEMP_PATH`
923+
and then loaded from there.
924+
914925
## Messenger
915926

916927
| Variable | Description | Default Value |
@@ -1052,14 +1063,16 @@ These environment variables only exist when you're using the official Docker Con
10521063
For more information on what these options do, please refer to
10531064
[the `pm2` documentation](https://pm2.keymetrics.io/docs/usage/application-declaration/).
10541065

1055-
| Variable | Description | Default |
1056-
| ------------------------ | ------------------------------------------------------------------ | ----------- |
1057-
| `PM2_INSTANCES` | Number of app instance to be launched | `1` |
1058-
| `PM2_EXEC_MODE` | One of `fork`, `cluster` | `'cluster'` |
1059-
| `PM2_MAX_MEMORY_RESTART` | App will be restarted if it exceeds the amount of memory specified ||
1060-
| `PM2_MIN_UPTIME` | Min uptime of the app to be considered started ||
1061-
| `PM2_LISTEN_TIMEOUT` | Time in ms before forcing a reload if app not listening ||
1062-
| `PM2_KILL_TIMEOUT` | Time in milliseconds before sending a final SIGKILL ||
1063-
| `PM2_MAX_RESTARTS` | Number of failed restarts before the process is killed |  — |
1064-
| `PM2_RESTART_DELAY` | Time to wait before restarting a crashed app | `0` |
1065-
| `PM2_AUTO_RESTART` | Automatically restart Directus if it crashes unexpectedly | `false` |
1066+
| Variable | Description | Default |
1067+
| ----------------------------- | ------------------------------------------------------------------ | ----------- |
1068+
| `PM2_INSTANCES`<sup>[1]</sup> | Number of app instance to be launched | `1` |
1069+
| `PM2_EXEC_MODE` | One of `fork`, `cluster` | `'cluster'` |
1070+
| `PM2_MAX_MEMORY_RESTART` | App will be restarted if it exceeds the amount of memory specified ||
1071+
| `PM2_MIN_UPTIME` | Min uptime of the app to be considered started ||
1072+
| `PM2_LISTEN_TIMEOUT` | Time in ms before forcing a reload if app not listening ||
1073+
| `PM2_KILL_TIMEOUT` | Time in milliseconds before sending a final SIGKILL ||
1074+
| `PM2_MAX_RESTARTS` | Number of failed restarts before the process is killed |  — |
1075+
| `PM2_RESTART_DELAY` | Time to wait before restarting a crashed app | `0` |
1076+
| `PM2_AUTO_RESTART` | Automatically restart Directus if it crashes unexpectedly | `false` |
1077+
1078+
<sup>[1]</sup> [Redis](#redis) is required in case of multiple instances.

‎packages/storage-driver-s3/src/index.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,17 @@ export class DriverS3 implements Driver {
200200
}
201201

202202
async *list(prefix = '') {
203+
let Prefix = this.fullPath(prefix);
204+
205+
// Current dir (`.`) isn't known to S3, needs to be an empty prefix instead
206+
if (Prefix === '.') Prefix = '';
207+
203208
let continuationToken: string | undefined = undefined;
204209

205210
do {
206211
const listObjectsV2CommandInput: ListObjectsV2CommandInput = {
207212
Bucket: this.config.bucket,
208-
Prefix: this.fullPath(prefix),
213+
Prefix,
209214
MaxKeys: 1000,
210215
};
211216

@@ -218,10 +223,14 @@ export class DriverS3 implements Driver {
218223
continuationToken = response.NextContinuationToken;
219224

220225
if (response.Contents) {
221-
for (const file of response.Contents) {
222-
if (file.Key) {
223-
yield file.Key.substring(this.root.length);
224-
}
226+
for (const object of response.Contents) {
227+
if (!object.Key) continue;
228+
229+
const isDir = object.Key.endsWith('/');
230+
231+
if (isDir) continue;
232+
233+
yield object.Key.substring(this.root.length);
225234
}
226235
}
227236
} while (continuationToken);

‎pnpm-lock.yaml

+11-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.