@directus/api 20.0.0-rc.0 → 20.0.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/app.js +9 -4
- package/dist/auth/drivers/ldap.js +4 -4
- package/dist/auth/drivers/local.js +4 -4
- package/dist/auth/drivers/oauth2.js +4 -4
- package/dist/auth/drivers/openid.js +4 -2
- package/dist/cache.js +0 -3
- package/dist/cli/commands/bootstrap/index.js +2 -8
- package/dist/cli/commands/init/index.js +10 -9
- package/dist/cli/utils/defaults.d.ts +11 -4
- package/dist/cli/utils/defaults.js +1 -7
- package/dist/constants.d.ts +9 -1
- package/dist/constants.js +10 -0
- package/dist/controllers/auth.js +16 -5
- package/dist/controllers/permissions.js +2 -14
- package/dist/controllers/roles.js +1 -22
- package/dist/controllers/{access.d.ts → tus.d.ts} +1 -0
- package/dist/controllers/tus.js +72 -0
- package/dist/controllers/users.js +55 -0
- package/dist/database/helpers/fn/types.d.ts +1 -2
- package/dist/database/helpers/fn/types.js +1 -1
- package/dist/database/helpers/geometry/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/geometry/dialects/mssql.js +2 -4
- package/dist/database/helpers/geometry/dialects/mysql.js +1 -1
- package/dist/database/helpers/geometry/dialects/oracle.d.ts +1 -1
- package/dist/database/helpers/geometry/dialects/oracle.js +3 -5
- package/dist/database/helpers/geometry/types.d.ts +1 -1
- package/dist/database/helpers/geometry/types.js +2 -4
- package/dist/database/index.js +1 -2
- package/dist/database/migrations/20240701A-add-tus-data.js +12 -0
- package/dist/database/{run-ast/types.d.ts → run-ast.d.ts} +9 -3
- package/dist/database/run-ast.js +450 -0
- package/dist/flows.js +4 -3
- package/dist/middleware/authenticate.js +7 -2
- package/dist/middleware/cache.js +1 -1
- package/dist/middleware/check-ip.d.ts +2 -0
- package/dist/middleware/check-ip.js +37 -0
- package/dist/middleware/get-permissions.d.ts +3 -0
- package/dist/middleware/get-permissions.js +10 -0
- package/dist/middleware/respond.js +1 -1
- package/dist/services/activity.js +10 -22
- package/dist/services/assets.d.ts +3 -2
- package/dist/services/assets.js +5 -10
- package/dist/services/authentication.js +26 -32
- package/dist/services/authorization.d.ts +17 -0
- package/dist/services/authorization.js +456 -0
- package/dist/services/collections.js +17 -18
- package/dist/services/fields.d.ts +1 -0
- package/dist/services/fields.js +24 -53
- package/dist/services/files/lib/extract-metadata.d.ts +3 -0
- package/dist/services/files/lib/extract-metadata.js +32 -0
- package/dist/services/files/utils/get-metadata.d.ts +5 -0
- package/dist/services/files/utils/get-metadata.js +107 -0
- package/dist/services/files.d.ts +4 -6
- package/dist/services/files.js +24 -140
- package/dist/services/graphql/index.d.ts +3 -3
- package/dist/services/graphql/index.js +22 -126
- package/dist/services/graphql/subscription.js +4 -2
- package/dist/services/import-export.js +4 -18
- package/dist/services/index.d.ts +2 -3
- package/dist/services/index.js +2 -3
- package/dist/services/items.js +44 -115
- package/dist/services/meta.js +23 -60
- package/dist/services/payload.d.ts +10 -9
- package/dist/services/payload.js +3 -18
- package/dist/services/{permissions.d.ts → permissions/index.d.ts} +7 -5
- package/dist/services/{permissions.js → permissions/index.js} +54 -30
- package/dist/{permissions → services/permissions}/lib/with-app-minimal-permissions.d.ts +1 -1
- package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
- package/dist/services/relations.d.ts +6 -0
- package/dist/services/relations.js +29 -26
- package/dist/services/roles.d.ts +12 -4
- package/dist/services/roles.js +424 -57
- package/dist/services/server.js +6 -0
- package/dist/services/shares.d.ts +2 -0
- package/dist/services/shares.js +8 -12
- package/dist/services/specifications.d.ts +2 -2
- package/dist/services/specifications.js +27 -39
- package/dist/services/tus/data-store.d.ts +36 -0
- package/dist/services/tus/data-store.js +214 -0
- package/dist/services/tus/index.d.ts +2 -0
- package/dist/services/tus/index.js +2 -0
- package/dist/services/tus/lockers.d.ts +36 -0
- package/dist/services/tus/lockers.js +83 -0
- package/dist/services/tus/server.d.ts +8 -0
- package/dist/services/tus/server.js +80 -0
- package/dist/services/tus/utils/wait-timeout.d.ts +1 -0
- package/dist/services/tus/utils/wait-timeout.js +13 -0
- package/dist/services/users.d.ts +5 -1
- package/dist/services/users.js +161 -78
- package/dist/services/utils.js +7 -11
- package/dist/services/versions.d.ts +2 -0
- package/dist/services/versions.js +10 -34
- package/dist/storage/register-locations.js +5 -1
- package/dist/telemetry/lib/get-report.js +2 -2
- package/dist/telemetry/utils/check-increased-user-limits.d.ts +7 -0
- package/dist/telemetry/utils/check-increased-user-limits.js +25 -0
- package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +6 -0
- package/dist/telemetry/utils/get-role-counts-by-roles.js +27 -0
- package/dist/telemetry/utils/get-role-counts-by-users.d.ts +11 -0
- package/dist/telemetry/utils/get-role-counts-by-users.js +34 -0
- package/dist/telemetry/utils/get-user-count.d.ts +8 -0
- package/dist/telemetry/utils/get-user-count.js +33 -0
- package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +7 -0
- package/dist/telemetry/utils/get-user-counts-by-roles.js +35 -0
- package/dist/types/ast.d.ts +1 -43
- package/dist/types/items.d.ts +0 -11
- package/dist/utils/apply-query.d.ts +3 -4
- package/dist/utils/apply-query.js +8 -37
- package/dist/utils/get-accountability-for-role.js +25 -16
- package/dist/utils/get-accountability-for-token.js +16 -17
- package/dist/utils/get-ast-from-query.d.ts +13 -0
- package/dist/utils/get-ast-from-query.js +297 -0
- package/dist/utils/get-cache-key.d.ts +1 -1
- package/dist/utils/get-cache-key.js +1 -12
- package/dist/utils/get-column.d.ts +1 -2
- package/dist/utils/get-column.js +0 -1
- package/dist/utils/get-permissions.d.ts +2 -0
- package/dist/utils/get-permissions.js +150 -0
- package/dist/utils/get-service.js +1 -5
- package/dist/utils/merge-permissions-for-share.d.ts +4 -0
- package/dist/utils/merge-permissions-for-share.js +109 -0
- package/dist/utils/merge-permissions.d.ts +3 -0
- package/dist/utils/merge-permissions.js +95 -0
- package/dist/utils/reduce-schema.d.ts +6 -4
- package/dist/utils/reduce-schema.js +34 -14
- package/dist/utils/verify-session-jwt.js +2 -1
- package/dist/websocket/authenticate.d.ts +2 -0
- package/dist/websocket/authenticate.js +12 -0
- package/dist/websocket/controllers/graphql.js +4 -1
- package/dist/websocket/controllers/hooks.js +0 -4
- package/dist/websocket/controllers/rest.js +2 -0
- package/dist/websocket/handlers/subscribe.js +2 -0
- package/dist/websocket/utils/items.d.ts +1 -1
- package/package.json +35 -33
- package/dist/controllers/access.js +0 -148
- package/dist/controllers/policies.d.ts +0 -2
- package/dist/controllers/policies.js +0 -169
- package/dist/database/get-ast-from-query/get-ast-from-query.d.ts +0 -16
- package/dist/database/get-ast-from-query/get-ast-from-query.js +0 -82
- package/dist/database/get-ast-from-query/lib/convert-wildcards.d.ts +0 -13
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +0 -69
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +0 -15
- package/dist/database/get-ast-from-query/lib/parse-fields.js +0 -190
- package/dist/database/get-ast-from-query/utils/get-deep-query.d.ts +0 -14
- package/dist/database/get-ast-from-query/utils/get-deep-query.js +0 -17
- package/dist/database/get-ast-from-query/utils/get-related-collection.d.ts +0 -2
- package/dist/database/get-ast-from-query/utils/get-related-collection.js +0 -13
- package/dist/database/get-ast-from-query/utils/get-relation.d.ts +0 -2
- package/dist/database/get-ast-from-query/utils/get-relation.js +0 -7
- package/dist/database/migrations/20240619A-permissions-policies.js +0 -163
- package/dist/database/run-ast/lib/get-db-query.d.ts +0 -4
- package/dist/database/run-ast/lib/get-db-query.js +0 -194
- package/dist/database/run-ast/lib/parse-current-level.d.ts +0 -7
- package/dist/database/run-ast/lib/parse-current-level.js +0 -41
- package/dist/database/run-ast/run-ast.d.ts +0 -7
- package/dist/database/run-ast/run-ast.js +0 -107
- package/dist/database/run-ast/types.js +0 -1
- package/dist/database/run-ast/utils/apply-case-when.d.ts +0 -16
- package/dist/database/run-ast/utils/apply-case-when.js +0 -26
- package/dist/database/run-ast/utils/apply-parent-filters.d.ts +0 -3
- package/dist/database/run-ast/utils/apply-parent-filters.js +0 -55
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +0 -10
- package/dist/database/run-ast/utils/get-column-pre-processor.js +0 -57
- package/dist/database/run-ast/utils/get-field-alias.d.ts +0 -2
- package/dist/database/run-ast/utils/get-field-alias.js +0 -4
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +0 -5
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +0 -23
- package/dist/database/run-ast/utils/merge-with-parent-items.d.ts +0 -3
- package/dist/database/run-ast/utils/merge-with-parent-items.js +0 -87
- package/dist/database/run-ast/utils/remove-temporary-fields.d.ts +0 -3
- package/dist/database/run-ast/utils/remove-temporary-fields.js +0 -73
- package/dist/permissions/cache.d.ts +0 -2
- package/dist/permissions/cache.js +0 -23
- package/dist/permissions/lib/fetch-permissions.d.ts +0 -10
- package/dist/permissions/lib/fetch-permissions.js +0 -55
- package/dist/permissions/lib/fetch-policies.d.ts +0 -7
- package/dist/permissions/lib/fetch-policies.js +0 -28
- package/dist/permissions/lib/fetch-roles-tree.d.ts +0 -3
- package/dist/permissions/lib/fetch-roles-tree.js +0 -28
- package/dist/permissions/lib/with-app-minimal-permissions.js +0 -10
- package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.d.ts +0 -7
- package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js +0 -56
- package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.d.ts +0 -3
- package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js +0 -16
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +0 -8
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +0 -24
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +0 -9
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +0 -31
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +0 -16
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +0 -27
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +0 -10
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +0 -23
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
- package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/types.js +0 -1
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +0 -12
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +0 -32
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +0 -4
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js +0 -29
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.d.ts +0 -4
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.js +0 -49
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.d.ts +0 -3
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.js +0 -56
- package/dist/permissions/modules/process-ast/lib/field-map-from-ast.d.ts +0 -4
- package/dist/permissions/modules/process-ast/lib/field-map-from-ast.js +0 -8
- package/dist/permissions/modules/process-ast/lib/inject-cases.d.ts +0 -9
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +0 -93
- package/dist/permissions/modules/process-ast/process-ast.d.ts +0 -9
- package/dist/permissions/modules/process-ast/process-ast.js +0 -39
- package/dist/permissions/modules/process-ast/types.d.ts +0 -24
- package/dist/permissions/modules/process-ast/types.js +0 -1
- package/dist/permissions/modules/process-ast/utils/collections-in-field-map.d.ts +0 -2
- package/dist/permissions/modules/process-ast/utils/collections-in-field-map.js +0 -7
- package/dist/permissions/modules/process-ast/utils/dedupe-access.d.ts +0 -12
- package/dist/permissions/modules/process-ast/utils/dedupe-access.js +0 -30
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.d.ts +0 -15
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +0 -50
- package/dist/permissions/modules/process-ast/utils/find-related-collection.d.ts +0 -3
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +0 -9
- package/dist/permissions/modules/process-ast/utils/flatten-filter.d.ts +0 -3
- package/dist/permissions/modules/process-ast/utils/flatten-filter.js +0 -24
- package/dist/permissions/modules/process-ast/utils/format-a2o-key.d.ts +0 -1
- package/dist/permissions/modules/process-ast/utils/format-a2o-key.js +0 -3
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +0 -5
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.js +0 -7
- package/dist/permissions/modules/process-ast/utils/has-item-permissions.d.ts +0 -2
- package/dist/permissions/modules/process-ast/utils/has-item-permissions.js +0 -3
- package/dist/permissions/modules/process-ast/utils/stringify-query-path.d.ts +0 -2
- package/dist/permissions/modules/process-ast/utils/stringify-query-path.js +0 -3
- package/dist/permissions/modules/process-ast/utils/validate-path/create-error.d.ts +0 -3
- package/dist/permissions/modules/process-ast/utils/validate-path/create-error.js +0 -16
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.d.ts +0 -2
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.js +0 -12
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.d.ts +0 -2
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.js +0 -28
- package/dist/permissions/modules/process-payload/lib/is-field-nullable.d.ts +0 -5
- package/dist/permissions/modules/process-payload/lib/is-field-nullable.js +0 -12
- package/dist/permissions/modules/process-payload/process-payload.d.ts +0 -13
- package/dist/permissions/modules/process-payload/process-payload.js +0 -77
- package/dist/permissions/modules/validate-access/lib/validate-collection-access.d.ts +0 -12
- package/dist/permissions/modules/validate-access/lib/validate-collection-access.js +0 -11
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +0 -9
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +0 -33
- package/dist/permissions/modules/validate-access/validate-access.d.ts +0 -14
- package/dist/permissions/modules/validate-access/validate-access.js +0 -28
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.d.ts +0 -1
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.js +0 -8
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +0 -5
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.js +0 -10
- package/dist/permissions/types.d.ts +0 -6
- package/dist/permissions/types.js +0 -1
- package/dist/permissions/utils/create-default-accountability.d.ts +0 -2
- package/dist/permissions/utils/create-default-accountability.js +0 -11
- package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +0 -8
- package/dist/permissions/utils/extract-required-dynamic-variable-context.js +0 -27
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -9
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +0 -43
- package/dist/permissions/utils/filter-policies-by-ip.d.ts +0 -2
- package/dist/permissions/utils/filter-policies-by-ip.js +0 -15
- package/dist/permissions/utils/get-unaliased-field-key.d.ts +0 -5
- package/dist/permissions/utils/get-unaliased-field-key.js +0 -17
- package/dist/permissions/utils/process-permissions.d.ts +0 -7
- package/dist/permissions/utils/process-permissions.js +0 -9
- package/dist/permissions/utils/with-cache.d.ts +0 -10
- package/dist/permissions/utils/with-cache.js +0 -25
- package/dist/services/access.d.ts +0 -10
- package/dist/services/access.js +0 -43
- package/dist/services/policies.d.ts +0 -12
- package/dist/services/policies.js +0 -87
- package/dist/telemetry/utils/check-user-limits.d.ts +0 -5
- package/dist/telemetry/utils/check-user-limits.js +0 -19
- package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +0 -17
- package/dist/utils/fetch-user-count/fetch-access-lookup.js +0 -22
- package/dist/utils/fetch-user-count/fetch-access-roles.d.ts +0 -16
- package/dist/utils/fetch-user-count/fetch-access-roles.js +0 -37
- package/dist/utils/fetch-user-count/fetch-active-users.d.ts +0 -6
- package/dist/utils/fetch-user-count/fetch-active-users.js +0 -3
- package/dist/utils/fetch-user-count/fetch-user-count.d.ts +0 -12
- package/dist/utils/fetch-user-count/fetch-user-count.js +0 -57
- package/dist/utils/fetch-user-count/get-user-count-query.d.ts +0 -20
- package/dist/utils/fetch-user-count/get-user-count-query.js +0 -17
- package/dist/utils/validate-user-count-integrity.d.ts +0 -13
- package/dist/utils/validate-user-count-integrity.js +0 -29
- /package/dist/database/migrations/{20240619A-permissions-policies.d.ts → 20240701A-add-tus-data.d.ts} +0 -0
- /package/dist/{utils → services/files/utils}/parse-image-metadata.d.ts +0 -0
- /package/dist/{utils → services/files/utils}/parse-image-metadata.js +0 -0
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import formatTitle from '@directus/format-title';
|
|
3
3
|
import { spec } from '@directus/specs';
|
|
4
|
-
import { isSystemCollection } from '@directus/system-data';
|
|
5
4
|
import { version } from 'directus/version';
|
|
6
5
|
import { cloneDeep, mergeWith } from 'lodash-es';
|
|
7
6
|
import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
|
|
8
7
|
import getDatabase from '../database/index.js';
|
|
9
|
-
import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
|
|
10
|
-
import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
|
|
11
|
-
import { fetchAllowedFieldMap } from '../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
|
|
12
8
|
import { getRelationType } from '../utils/get-relation-type.js';
|
|
13
9
|
import { reduceSchema } from '../utils/reduce-schema.js';
|
|
14
10
|
import { GraphQLService } from './graphql/index.js';
|
|
11
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
15
12
|
const env = useEnv();
|
|
16
13
|
export class SpecificationService {
|
|
17
14
|
accountability;
|
|
@@ -19,38 +16,29 @@ export class SpecificationService {
|
|
|
19
16
|
schema;
|
|
20
17
|
oas;
|
|
21
18
|
graphql;
|
|
22
|
-
constructor(
|
|
23
|
-
this.accountability =
|
|
24
|
-
this.knex =
|
|
25
|
-
this.schema =
|
|
26
|
-
this.oas = new OASSpecsService(
|
|
27
|
-
this.graphql = new GraphQLSpecsService(
|
|
19
|
+
constructor({ accountability, knex, schema }) {
|
|
20
|
+
this.accountability = accountability || null;
|
|
21
|
+
this.knex = knex || getDatabase();
|
|
22
|
+
this.schema = schema;
|
|
23
|
+
this.oas = new OASSpecsService({ knex, schema, accountability });
|
|
24
|
+
this.graphql = new GraphQLSpecsService({ knex, schema, accountability });
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
class OASSpecsService {
|
|
31
28
|
accountability;
|
|
32
29
|
knex;
|
|
33
30
|
schema;
|
|
34
|
-
constructor(
|
|
35
|
-
this.accountability =
|
|
36
|
-
this.knex =
|
|
37
|
-
this.schema =
|
|
31
|
+
constructor({ knex, schema, accountability }) {
|
|
32
|
+
this.accountability = accountability || null;
|
|
33
|
+
this.knex = knex || getDatabase();
|
|
34
|
+
this.schema =
|
|
35
|
+
this.accountability?.admin === true ? schema : reduceSchema(schema, accountability?.permissions || null);
|
|
38
36
|
}
|
|
39
37
|
async generate(host) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (this.accountability && this.accountability.admin !== true) {
|
|
43
|
-
const allowedFields = await fetchAllowedFieldMap({
|
|
44
|
-
accountability: this.accountability,
|
|
45
|
-
action: 'read',
|
|
46
|
-
}, { schema, knex: this.knex });
|
|
47
|
-
schema = reduceSchema(schema, allowedFields);
|
|
48
|
-
const policies = await fetchPolicies(this.accountability, { schema, knex: this.knex });
|
|
49
|
-
permissions = await fetchPermissions({ action: 'read', policies, accountability: this.accountability }, { schema, knex: this.knex });
|
|
50
|
-
}
|
|
51
|
-
const tags = await this.generateTags(schema);
|
|
38
|
+
const permissions = this.accountability?.permissions ?? [];
|
|
39
|
+
const tags = await this.generateTags();
|
|
52
40
|
const paths = await this.generatePaths(permissions, tags);
|
|
53
|
-
const components = await this.generateComponents(
|
|
41
|
+
const components = await this.generateComponents(tags);
|
|
54
42
|
const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
|
|
55
43
|
const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
|
|
56
44
|
const spec = {
|
|
@@ -74,9 +62,9 @@ class OASSpecsService {
|
|
|
74
62
|
spec.components = components;
|
|
75
63
|
return spec;
|
|
76
64
|
}
|
|
77
|
-
async generateTags(
|
|
65
|
+
async generateTags() {
|
|
78
66
|
const systemTags = cloneDeep(spec.tags);
|
|
79
|
-
const collections = Object.values(schema.collections);
|
|
67
|
+
const collections = Object.values(this.schema.collections);
|
|
80
68
|
const tags = [];
|
|
81
69
|
for (const systemTag of systemTags) {
|
|
82
70
|
// Check if necessary authentication level is given
|
|
@@ -258,7 +246,7 @@ class OASSpecsService {
|
|
|
258
246
|
}
|
|
259
247
|
return paths;
|
|
260
248
|
}
|
|
261
|
-
async generateComponents(
|
|
249
|
+
async generateComponents(tags) {
|
|
262
250
|
if (!tags)
|
|
263
251
|
return;
|
|
264
252
|
let components = cloneDeep(spec.components);
|
|
@@ -276,7 +264,7 @@ class OASSpecsService {
|
|
|
276
264
|
};
|
|
277
265
|
}
|
|
278
266
|
}
|
|
279
|
-
const collections = Object.values(schema.collections);
|
|
267
|
+
const collections = Object.values(this.schema.collections);
|
|
280
268
|
for (const collection of collections) {
|
|
281
269
|
const tag = tags.find((tag) => tag['x-collection'] === collection.collection);
|
|
282
270
|
if (!tag)
|
|
@@ -289,7 +277,7 @@ class OASSpecsService {
|
|
|
289
277
|
schemaComponent['x-collection'] = collection.collection;
|
|
290
278
|
for (const field of fieldsInCollection) {
|
|
291
279
|
schemaComponent.properties[field.field] =
|
|
292
|
-
cloneDeep(spec.components.schemas[tag.name].properties[field.field]) || this.generateField(
|
|
280
|
+
cloneDeep(spec.components.schemas[tag.name].properties[field.field]) || this.generateField(collection.collection, field, tags);
|
|
293
281
|
}
|
|
294
282
|
components.schemas[tag.name] = schemaComponent;
|
|
295
283
|
}
|
|
@@ -300,7 +288,7 @@ class OASSpecsService {
|
|
|
300
288
|
'x-collection': collection.collection,
|
|
301
289
|
};
|
|
302
290
|
for (const field of fieldsInCollection) {
|
|
303
|
-
schemaComponent.properties[field.field] = this.generateField(
|
|
291
|
+
schemaComponent.properties[field.field] = this.generateField(collection.collection, field, tags);
|
|
304
292
|
}
|
|
305
293
|
components.schemas[tag.name] = schemaComponent;
|
|
306
294
|
}
|
|
@@ -323,13 +311,13 @@ class OASSpecsService {
|
|
|
323
311
|
return 'read';
|
|
324
312
|
}
|
|
325
313
|
}
|
|
326
|
-
generateField(
|
|
314
|
+
generateField(collection, field, tags) {
|
|
327
315
|
let propertyObject = {};
|
|
328
316
|
propertyObject.nullable = field.nullable;
|
|
329
317
|
if (field.note) {
|
|
330
318
|
propertyObject.description = field.note;
|
|
331
319
|
}
|
|
332
|
-
const relation = schema.relations.find((relation) => (relation.collection === collection && relation.field === field.field) ||
|
|
320
|
+
const relation = this.schema.relations.find((relation) => (relation.collection === collection && relation.field === field.field) ||
|
|
333
321
|
(relation.related_collection === collection && relation.meta?.one_field === field.field));
|
|
334
322
|
if (!relation) {
|
|
335
323
|
propertyObject = {
|
|
@@ -347,10 +335,10 @@ class OASSpecsService {
|
|
|
347
335
|
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.related_collection);
|
|
348
336
|
if (!relatedTag ||
|
|
349
337
|
!relation.related_collection ||
|
|
350
|
-
relation.related_collection in schema.collections === false) {
|
|
338
|
+
relation.related_collection in this.schema.collections === false) {
|
|
351
339
|
return propertyObject;
|
|
352
340
|
}
|
|
353
|
-
const relatedCollection = schema.collections[relation.related_collection];
|
|
341
|
+
const relatedCollection = this.schema.collections[relation.related_collection];
|
|
354
342
|
const relatedPrimaryKeyField = relatedCollection.fields[relatedCollection.primary];
|
|
355
343
|
propertyObject.oneOf = [
|
|
356
344
|
{
|
|
@@ -363,10 +351,10 @@ class OASSpecsService {
|
|
|
363
351
|
}
|
|
364
352
|
else if (relationType === 'o2m') {
|
|
365
353
|
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.collection);
|
|
366
|
-
if (!relatedTag || !relation.related_collection || relation.collection in schema.collections === false) {
|
|
354
|
+
if (!relatedTag || !relation.related_collection || relation.collection in this.schema.collections === false) {
|
|
367
355
|
return propertyObject;
|
|
368
356
|
}
|
|
369
|
-
const relatedCollection = schema.collections[relation.collection];
|
|
357
|
+
const relatedCollection = this.schema.collections[relation.collection];
|
|
370
358
|
const relatedPrimaryKeyField = relatedCollection.fields[relatedCollection.primary];
|
|
371
359
|
if (!relatedTag || !relatedPrimaryKeyField)
|
|
372
360
|
return propertyObject;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { TusDriver } from '@directus/storage';
|
|
3
|
+
import type { Accountability, File, SchemaOverview } from '@directus/types';
|
|
4
|
+
import stream from 'node:stream';
|
|
5
|
+
import { DataStore, Upload } from '@tus/utils';
|
|
6
|
+
export type TusDataStoreConfig = {
|
|
7
|
+
constants: {
|
|
8
|
+
ENABLED: boolean;
|
|
9
|
+
CHUNK_SIZE: number;
|
|
10
|
+
MAX_SIZE: number;
|
|
11
|
+
EXPIRATION_TIME: number;
|
|
12
|
+
SCHEDULE: string;
|
|
13
|
+
};
|
|
14
|
+
/** Storage location name **/
|
|
15
|
+
location: string;
|
|
16
|
+
driver: TusDriver;
|
|
17
|
+
schema: SchemaOverview;
|
|
18
|
+
accountability: Accountability | undefined;
|
|
19
|
+
};
|
|
20
|
+
export declare class TusDataStore extends DataStore {
|
|
21
|
+
protected chunkSize: number;
|
|
22
|
+
protected maxSize: number;
|
|
23
|
+
protected expirationTime: number;
|
|
24
|
+
protected location: string;
|
|
25
|
+
protected storageDriver: TusDriver;
|
|
26
|
+
protected schema: SchemaOverview;
|
|
27
|
+
protected accountability: Accountability | undefined;
|
|
28
|
+
constructor(config: TusDataStoreConfig);
|
|
29
|
+
create(upload: Upload): Promise<Upload>;
|
|
30
|
+
write(readable: stream.Readable, tus_id: string, offset: number): Promise<number>;
|
|
31
|
+
remove(tus_id: string): Promise<void>;
|
|
32
|
+
deleteExpired(): Promise<number>;
|
|
33
|
+
getExpiration(): number;
|
|
34
|
+
getUpload(id: string): Promise<Upload>;
|
|
35
|
+
protected getFileById(tus_id: string): Promise<File>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import formatTitle from '@directus/format-title';
|
|
2
|
+
import { extension } from 'mime-types';
|
|
3
|
+
import { extname } from 'node:path';
|
|
4
|
+
import stream from 'node:stream';
|
|
5
|
+
import { DataStore, ERRORS, Upload } from '@tus/utils';
|
|
6
|
+
import { ItemsService } from '../items.js';
|
|
7
|
+
import { useLogger } from '../../logger.js';
|
|
8
|
+
import getDatabase from '../../database/index.js';
|
|
9
|
+
import { omit } from 'lodash-es';
|
|
10
|
+
export class TusDataStore extends DataStore {
|
|
11
|
+
chunkSize;
|
|
12
|
+
maxSize;
|
|
13
|
+
expirationTime;
|
|
14
|
+
location;
|
|
15
|
+
storageDriver;
|
|
16
|
+
schema;
|
|
17
|
+
accountability;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
super();
|
|
20
|
+
this.chunkSize = config.constants.CHUNK_SIZE;
|
|
21
|
+
this.maxSize = config.constants.MAX_SIZE;
|
|
22
|
+
this.expirationTime = config.constants.EXPIRATION_TIME;
|
|
23
|
+
this.location = config.location;
|
|
24
|
+
this.storageDriver = config.driver;
|
|
25
|
+
this.extensions = this.storageDriver.tusExtensions;
|
|
26
|
+
this.schema = config.schema;
|
|
27
|
+
this.accountability = config.accountability;
|
|
28
|
+
}
|
|
29
|
+
async create(upload) {
|
|
30
|
+
const logger = useLogger();
|
|
31
|
+
const knex = getDatabase();
|
|
32
|
+
const itemsService = new ItemsService('directus_files', {
|
|
33
|
+
accountability: this.accountability,
|
|
34
|
+
schema: this.schema,
|
|
35
|
+
knex,
|
|
36
|
+
});
|
|
37
|
+
upload.creation_date = new Date().toISOString();
|
|
38
|
+
if (!upload.size || !upload.metadata || !upload.metadata['filename_download']) {
|
|
39
|
+
throw ERRORS.INVALID_METADATA;
|
|
40
|
+
}
|
|
41
|
+
if (!upload.metadata['type']) {
|
|
42
|
+
upload.metadata['type'] = 'application/octet-stream';
|
|
43
|
+
}
|
|
44
|
+
if (!upload.metadata['title']) {
|
|
45
|
+
upload.metadata['title'] = formatTitle(upload.metadata['filename_download']);
|
|
46
|
+
}
|
|
47
|
+
let existingFile = null;
|
|
48
|
+
// If the payload contains a primary key, we'll check if the file already exists
|
|
49
|
+
if (upload.metadata['id']) {
|
|
50
|
+
// 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
|
|
51
|
+
existingFile =
|
|
52
|
+
(await knex
|
|
53
|
+
.select('folder', 'filename_download', 'filename_disk', 'title', 'description', 'metadata', 'tus_id')
|
|
54
|
+
.from('directus_files')
|
|
55
|
+
.andWhere({ id: upload.metadata['id'] })
|
|
56
|
+
.first()) ?? null;
|
|
57
|
+
if (existingFile && existingFile['tus_id'] !== null) {
|
|
58
|
+
throw ERRORS.INVALID_METADATA;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Is this file a replacement? if the file data already exists and we have a primary key
|
|
62
|
+
const isReplacement = existingFile !== null && !!upload.metadata['id'];
|
|
63
|
+
if (isReplacement === true && upload.metadata['id']) {
|
|
64
|
+
upload.metadata['replace_id'] = upload.metadata['id'];
|
|
65
|
+
}
|
|
66
|
+
const fileData = {
|
|
67
|
+
...omit(upload.metadata, ['id']),
|
|
68
|
+
tus_id: upload.id,
|
|
69
|
+
tus_data: upload,
|
|
70
|
+
filesize: upload.size,
|
|
71
|
+
storage: this.location,
|
|
72
|
+
};
|
|
73
|
+
// If no folder is specified, we'll use the default folder from the settings if it exists
|
|
74
|
+
if ('folder' in fileData === false) {
|
|
75
|
+
const settings = await knex.select('storage_default_folder').from('directus_settings').first();
|
|
76
|
+
if (settings?.storage_default_folder) {
|
|
77
|
+
fileData.folder = settings.storage_default_folder;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// If this is a new file upload, we need to generate a new primary key and DB record
|
|
81
|
+
const primaryKey = await itemsService.createOne(fileData, { emitEvents: false });
|
|
82
|
+
const fileExtension = extname(upload.metadata['filename_download']) ||
|
|
83
|
+
(upload.metadata['type'] && '.' + extension(upload.metadata['type'])) ||
|
|
84
|
+
'';
|
|
85
|
+
// The filename_disk is the FINAL filename on disk
|
|
86
|
+
fileData.filename_disk ||= primaryKey + (fileExtension || '');
|
|
87
|
+
// Temp filename is used for replacements
|
|
88
|
+
// const tempFilenameDisk = fileData.tus_id! + (fileExtension || '');
|
|
89
|
+
// if (isReplacement) {
|
|
90
|
+
// upload.metadata['temp_file'] = tempFilenameDisk;
|
|
91
|
+
// }
|
|
92
|
+
try {
|
|
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
|
+
upload = (await this.storageDriver.createChunkedUpload(fileData.filename_disk, upload));
|
|
95
|
+
fileData.tus_data = upload;
|
|
96
|
+
await itemsService.updateOne(primaryKey, fileData, { emitEvents: false });
|
|
97
|
+
return upload;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.warn(`Couldn't create chunked upload for ${fileData.filename_disk}`);
|
|
101
|
+
logger.warn(err);
|
|
102
|
+
if (isReplacement) {
|
|
103
|
+
await itemsService.updateOne(primaryKey, { tus_id: null, tus_data: null }, { emitEvents: false });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await itemsService.deleteOne(primaryKey, { emitEvents: false });
|
|
107
|
+
}
|
|
108
|
+
throw ERRORS.UNKNOWN_ERROR;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async write(readable, tus_id, offset) {
|
|
112
|
+
const fileData = await this.getFileById(tus_id);
|
|
113
|
+
const filePath = fileData.filename_disk;
|
|
114
|
+
const sudoService = new ItemsService('directus_files', {
|
|
115
|
+
schema: this.schema,
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
const newOffset = await this.storageDriver.writeChunk(filePath, readable, offset, fileData.tus_data);
|
|
119
|
+
await sudoService.updateOne(fileData.id, {
|
|
120
|
+
tus_data: {
|
|
121
|
+
...fileData.tus_data,
|
|
122
|
+
offset: newOffset,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (Number(fileData.filesize) === newOffset) {
|
|
126
|
+
try {
|
|
127
|
+
await this.storageDriver.finishChunkedUpload(filePath, fileData.tus_data);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
await this.remove(fileData.tus_id);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
const isReplacement = Boolean(fileData.tus_data?.['metadata']?.['replace_id']);
|
|
134
|
+
// If the file is a replacement, delete the old files, and upgrade the temp file
|
|
135
|
+
if (isReplacement === true) {
|
|
136
|
+
const replaceId = fileData.tus_data['metadata']['replace_id'];
|
|
137
|
+
const replaceData = await sudoService.readOne(replaceId, { fields: ['filename_disk'] });
|
|
138
|
+
// delete the previously saved file and thumbnails to ensure they're generated fresh
|
|
139
|
+
for await (const partPath of this.storageDriver.list(replaceId)) {
|
|
140
|
+
await this.storageDriver.delete(partPath);
|
|
141
|
+
}
|
|
142
|
+
// Upgrade the temp file to the final filename
|
|
143
|
+
await this.storageDriver.move(filePath, replaceData.filename_disk);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return newOffset;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if ('status_code' in err && err.status_code === 500) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
throw ERRORS.FILE_WRITE_ERROR;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async remove(tus_id) {
|
|
156
|
+
const sudoService = new ItemsService('directus_files', {
|
|
157
|
+
schema: this.schema,
|
|
158
|
+
});
|
|
159
|
+
const fileData = await this.getFileById(tus_id);
|
|
160
|
+
await this.storageDriver.deleteChunkedUpload(fileData.filename_disk, fileData.tus_data);
|
|
161
|
+
await sudoService.deleteOne(fileData.id);
|
|
162
|
+
}
|
|
163
|
+
async deleteExpired() {
|
|
164
|
+
const sudoService = new ItemsService('directus_files', {
|
|
165
|
+
schema: this.schema,
|
|
166
|
+
});
|
|
167
|
+
const now = new Date();
|
|
168
|
+
const toDelete = [];
|
|
169
|
+
const uploadFiles = await sudoService.readByQuery({
|
|
170
|
+
fields: ['modified_on', 'tus_id', 'tus_data'],
|
|
171
|
+
filter: { tus_id: { _nnull: true } },
|
|
172
|
+
});
|
|
173
|
+
if (!uploadFiles)
|
|
174
|
+
return 0;
|
|
175
|
+
for (const fileData of uploadFiles) {
|
|
176
|
+
if (fileData &&
|
|
177
|
+
fileData.tus_data &&
|
|
178
|
+
this.getExpiration() > 0 &&
|
|
179
|
+
fileData.tus_data['size'] !== fileData.tus_data['offset'] &&
|
|
180
|
+
fileData.modified_on) {
|
|
181
|
+
const modified = new Date(fileData.modified_on);
|
|
182
|
+
const expires = new Date(modified.getTime() + this.getExpiration());
|
|
183
|
+
if (now > expires) {
|
|
184
|
+
toDelete.push(this.remove(fileData.tus_id));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
await Promise.allSettled(toDelete);
|
|
189
|
+
return toDelete.length;
|
|
190
|
+
}
|
|
191
|
+
getExpiration() {
|
|
192
|
+
return this.expirationTime;
|
|
193
|
+
}
|
|
194
|
+
async getUpload(id) {
|
|
195
|
+
const fileData = await this.getFileById(id);
|
|
196
|
+
return new Upload(fileData.tus_data);
|
|
197
|
+
}
|
|
198
|
+
async getFileById(tus_id) {
|
|
199
|
+
const itemsService = new ItemsService('directus_files', {
|
|
200
|
+
schema: this.schema,
|
|
201
|
+
});
|
|
202
|
+
const results = await itemsService.readByQuery({
|
|
203
|
+
filter: {
|
|
204
|
+
tus_id: { _eq: tus_id },
|
|
205
|
+
storage: { _eq: this.location },
|
|
206
|
+
...(this.accountability?.user ? { uploaded_by: { _eq: this.accountability.user } } : {}),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (!results || !results[0]) {
|
|
210
|
+
throw ERRORS.FILE_NOT_FOUND;
|
|
211
|
+
}
|
|
212
|
+
return results[0];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Lock, type Locker, type RequestRelease } from '@tus/utils';
|
|
2
|
+
/**
|
|
3
|
+
* TusLocker is an implementation of the Locker interface that manages locks in memory or using Redis.
|
|
4
|
+
* This class is designed for exclusive access control over resources, often used in scenarios like upload management.
|
|
5
|
+
*
|
|
6
|
+
* Locking Behavior:
|
|
7
|
+
* - When the `lock` method is invoked for an already locked resource, the `cancelReq` callback is called.
|
|
8
|
+
* This signals to the current lock holder that another process is requesting the lock, encouraging them to release it as soon as possible.
|
|
9
|
+
* - The lock attempt continues until the specified timeout is reached. If the timeout expires and the lock is still not
|
|
10
|
+
* available, an error is thrown to indicate lock acquisition failure.
|
|
11
|
+
*
|
|
12
|
+
* Lock Acquisition and Release:
|
|
13
|
+
* - The `lock` method implements a wait mechanism, allowing a lock request to either succeed when the lock becomes available,
|
|
14
|
+
* or fail after the timeout period.
|
|
15
|
+
* - The `unlock` method releases a lock, making the resource available for other requests.
|
|
16
|
+
*/
|
|
17
|
+
export declare class TusLocker implements Locker {
|
|
18
|
+
lockTimeout: number;
|
|
19
|
+
acquireTimeout: number;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
acquireLockTimeout: number;
|
|
22
|
+
lockTimeout: number;
|
|
23
|
+
});
|
|
24
|
+
newLock(id: string): KvLock;
|
|
25
|
+
}
|
|
26
|
+
export declare class KvLock implements Lock {
|
|
27
|
+
private id;
|
|
28
|
+
private lockTimeout;
|
|
29
|
+
private acquireTimeout;
|
|
30
|
+
private kv;
|
|
31
|
+
constructor(id: string, lockTimeout?: number, acquireTimeout?: number);
|
|
32
|
+
lock(cancelReq: RequestRelease): Promise<void>;
|
|
33
|
+
protected acquireLock(id: string, requestRelease: RequestRelease, signal: AbortSignal): Promise<boolean>;
|
|
34
|
+
unlock(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function getTusLocker(): Locker;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ERRORS } from '@tus/utils';
|
|
2
|
+
import { useLock } from '../../lock/index.js';
|
|
3
|
+
import { waitTimeout } from './utils/wait-timeout.js';
|
|
4
|
+
/**
|
|
5
|
+
* TusLocker is an implementation of the Locker interface that manages locks in memory or using Redis.
|
|
6
|
+
* This class is designed for exclusive access control over resources, often used in scenarios like upload management.
|
|
7
|
+
*
|
|
8
|
+
* Locking Behavior:
|
|
9
|
+
* - When the `lock` method is invoked for an already locked resource, the `cancelReq` callback is called.
|
|
10
|
+
* This signals to the current lock holder that another process is requesting the lock, encouraging them to release it as soon as possible.
|
|
11
|
+
* - The lock attempt continues until the specified timeout is reached. If the timeout expires and the lock is still not
|
|
12
|
+
* available, an error is thrown to indicate lock acquisition failure.
|
|
13
|
+
*
|
|
14
|
+
* Lock Acquisition and Release:
|
|
15
|
+
* - The `lock` method implements a wait mechanism, allowing a lock request to either succeed when the lock becomes available,
|
|
16
|
+
* or fail after the timeout period.
|
|
17
|
+
* - The `unlock` method releases a lock, making the resource available for other requests.
|
|
18
|
+
*/
|
|
19
|
+
export class TusLocker {
|
|
20
|
+
lockTimeout;
|
|
21
|
+
acquireTimeout;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.acquireTimeout = options?.acquireLockTimeout ?? 1000 * 30;
|
|
24
|
+
this.lockTimeout = options?.lockTimeout ?? 1000 * 60;
|
|
25
|
+
}
|
|
26
|
+
newLock(id) {
|
|
27
|
+
return new KvLock(id, this.lockTimeout, this.acquireTimeout);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class KvLock {
|
|
31
|
+
id;
|
|
32
|
+
lockTimeout;
|
|
33
|
+
acquireTimeout;
|
|
34
|
+
kv;
|
|
35
|
+
constructor(id, lockTimeout = 1000 * 60, acquireTimeout = 1000 * 30) {
|
|
36
|
+
this.id = id;
|
|
37
|
+
this.lockTimeout = lockTimeout;
|
|
38
|
+
this.acquireTimeout = acquireTimeout;
|
|
39
|
+
this.kv = useLock();
|
|
40
|
+
}
|
|
41
|
+
async lock(cancelReq) {
|
|
42
|
+
const abortController = new AbortController();
|
|
43
|
+
const lock = await Promise.race([
|
|
44
|
+
waitTimeout(this.acquireTimeout, abortController.signal),
|
|
45
|
+
this.acquireLock(this.id, cancelReq, abortController.signal),
|
|
46
|
+
]);
|
|
47
|
+
abortController.abort();
|
|
48
|
+
if (!lock) {
|
|
49
|
+
throw ERRORS.ERR_LOCK_TIMEOUT;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async acquireLock(id, requestRelease, signal) {
|
|
53
|
+
if (signal.aborted) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const lockTime = await this.kv.get(id);
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (!lockTime || Number(lockTime) < now - this.lockTimeout) {
|
|
59
|
+
await this.kv.set(id, now);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
await requestRelease();
|
|
63
|
+
return await new Promise((resolve, reject) => {
|
|
64
|
+
// Using setImmediate to:
|
|
65
|
+
// 1. Prevent stack overflow by deferring recursive calls to the next event loop iteration.
|
|
66
|
+
// 2. Allow event loop to process other pending events, maintaining server responsiveness.
|
|
67
|
+
// 3. Ensure fairness in lock acquisition by giving other requests a chance to acquire the lock.
|
|
68
|
+
setImmediate(() => {
|
|
69
|
+
this.acquireLock(id, requestRelease, signal).then(resolve).catch(reject);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async unlock() {
|
|
74
|
+
await this.kv.delete(this.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let _locker = undefined;
|
|
78
|
+
export function getTusLocker() {
|
|
79
|
+
if (!_locker) {
|
|
80
|
+
_locker = new TusLocker();
|
|
81
|
+
}
|
|
82
|
+
return _locker;
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
|
+
import { Server } from '@tus/server';
|
|
3
|
+
type Context = {
|
|
4
|
+
schema: SchemaOverview;
|
|
5
|
+
accountability?: Accountability | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare function createTusServer(context: Context): Promise<Server>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUS implementation for resumable uploads
|
|
3
|
+
*
|
|
4
|
+
* https://tus.io/
|
|
5
|
+
*/
|
|
6
|
+
import { useEnv } from '@directus/env';
|
|
7
|
+
import { supportsTus } from '@directus/storage';
|
|
8
|
+
import { toArray } from '@directus/utils';
|
|
9
|
+
import { Server } from '@tus/server';
|
|
10
|
+
import { RESUMABLE_UPLOADS } from '../../constants.js';
|
|
11
|
+
import { getStorage } from '../../storage/index.js';
|
|
12
|
+
import { extractMetadata } from '../files/lib/extract-metadata.js';
|
|
13
|
+
import { ItemsService } from '../index.js';
|
|
14
|
+
import { TusDataStore } from './data-store.js';
|
|
15
|
+
import { getTusLocker } from './lockers.js';
|
|
16
|
+
import { pick } from 'lodash-es';
|
|
17
|
+
async function createTusStore(context) {
|
|
18
|
+
const env = useEnv();
|
|
19
|
+
const storage = await getStorage();
|
|
20
|
+
const location = toArray(env['STORAGE_LOCATIONS'])[0];
|
|
21
|
+
const driver = storage.location(location);
|
|
22
|
+
if (!supportsTus(driver)) {
|
|
23
|
+
throw new Error(`Storage location ${location} does not support the TUS protocol`);
|
|
24
|
+
}
|
|
25
|
+
return new TusDataStore({
|
|
26
|
+
constants: RESUMABLE_UPLOADS,
|
|
27
|
+
accountability: context.accountability,
|
|
28
|
+
schema: context.schema,
|
|
29
|
+
location,
|
|
30
|
+
driver,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function createTusServer(context) {
|
|
34
|
+
const env = useEnv();
|
|
35
|
+
const store = await createTusStore(context);
|
|
36
|
+
return new Server({
|
|
37
|
+
path: '/files/tus',
|
|
38
|
+
datastore: store,
|
|
39
|
+
locker: getTusLocker(),
|
|
40
|
+
maxSize: RESUMABLE_UPLOADS.MAX_SIZE,
|
|
41
|
+
async onUploadFinish(req, res, upload) {
|
|
42
|
+
const service = new ItemsService('directus_files', {
|
|
43
|
+
schema: req.schema,
|
|
44
|
+
});
|
|
45
|
+
const file = (await service.readByQuery({
|
|
46
|
+
filter: { tus_id: { _eq: upload.id } },
|
|
47
|
+
limit: 1,
|
|
48
|
+
}))[0];
|
|
49
|
+
if (!file)
|
|
50
|
+
return res;
|
|
51
|
+
// update metadata when file is replaced
|
|
52
|
+
if (file.tus_data?.['metadata']?.['replace_id']) {
|
|
53
|
+
const newFile = await service.readOne(file.tus_data['metadata']['replace_id']);
|
|
54
|
+
const updateFields = pick(file, ['filename_download', 'filesize', 'type']);
|
|
55
|
+
const metadata = await extractMetadata(newFile.storage, {
|
|
56
|
+
...newFile,
|
|
57
|
+
...updateFields,
|
|
58
|
+
});
|
|
59
|
+
await service.updateOne(file.tus_data['metadata']['replace_id'], {
|
|
60
|
+
...updateFields,
|
|
61
|
+
...metadata,
|
|
62
|
+
});
|
|
63
|
+
await service.deleteOne(file.id);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const metadata = await extractMetadata(file.storage, file);
|
|
67
|
+
await service.updateOne(file.id, {
|
|
68
|
+
...metadata,
|
|
69
|
+
tus_id: null,
|
|
70
|
+
tus_data: null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return res;
|
|
74
|
+
},
|
|
75
|
+
generateUrl(_req, opts) {
|
|
76
|
+
return env['PUBLIC_URL'] + '/files/tus/' + opts.id;
|
|
77
|
+
},
|
|
78
|
+
relativeLocation: String(env['PUBLIC_URL']).startsWith('http'),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function waitTimeout(timeout: number, signal: AbortSignal): Promise<boolean>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function waitTimeout(timeout, signal) {
|
|
2
|
+
return new Promise((resolve) => {
|
|
3
|
+
const handler = setTimeout(() => {
|
|
4
|
+
resolve(false);
|
|
5
|
+
}, timeout);
|
|
6
|
+
const abortListener = () => {
|
|
7
|
+
clearTimeout(handler);
|
|
8
|
+
signal.removeEventListener('abort', abortListener);
|
|
9
|
+
resolve(false);
|
|
10
|
+
};
|
|
11
|
+
signal.addEventListener('abort', abortListener);
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/services/users.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export declare class UsersService extends ItemsService {
|
|
|
13
13
|
* directus_settings.auth_password_policy
|
|
14
14
|
*/
|
|
15
15
|
private checkPasswordPolicy;
|
|
16
|
+
private checkRemainingAdminExistence;
|
|
17
|
+
/**
|
|
18
|
+
* Make sure there's at least one active admin user when updating user status
|
|
19
|
+
*/
|
|
20
|
+
private checkRemainingActiveAdmin;
|
|
16
21
|
/**
|
|
17
22
|
* Get basic information of user identified by email
|
|
18
23
|
*/
|
|
@@ -47,5 +52,4 @@ export declare class UsersService extends ItemsService {
|
|
|
47
52
|
verifyRegistration(token: string): Promise<string>;
|
|
48
53
|
requestPasswordReset(email: string, url: string | null, subject?: string | null): Promise<void>;
|
|
49
54
|
resetPassword(token: string, password: string): Promise<void>;
|
|
50
|
-
private clearCaches;
|
|
51
55
|
}
|