@jrmc/adonis-attachment 3.3.0-beta.3 → 3.3.0-beta.5
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/README.md +3 -3
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/build/services/regenerate_service.d.ts +8 -0
- package/build/services/regenerate_service.js +29 -0
- package/build/src/attachment_manager.d.ts +3 -4
- package/build/src/attachment_manager.js +2 -5
- package/build/src/attachments/attachment.js +11 -5
- package/build/src/attachments/attachment_base.d.ts +4 -1
- package/build/src/attachments/attachment_base.js +38 -3
- package/build/src/attachments/variant_attachment.d.ts +2 -0
- package/build/src/attachments/variant_attachment.js +4 -0
- package/build/src/converter_manager.d.ts +2 -2
- package/build/src/converter_manager.js +88 -48
- package/build/src/converters/autodetect_converter.d.ts +2 -1
- package/build/src/converters/autodetect_converter.js +7 -9
- package/build/src/converters/converter.d.ts +3 -1
- package/build/src/converters/converter.js +3 -0
- package/build/src/converters/video_thumbnail_converter.d.ts +2 -2
- package/build/src/converters/video_thumbnail_converter.js +1 -1
- package/build/src/decorators/attachment.d.ts +2 -2
- package/build/src/define_config.d.ts +2 -2
- package/build/src/define_config.js +8 -4
- package/build/src/mixins/attachmentable.d.ts +4 -4
- package/build/src/mixins/attachmentable.js +3 -3
- package/build/src/services/record_with_attachment.d.ts +6 -3
- package/build/src/services/record_with_attachment.js +88 -42
- package/build/src/types/attachment.d.ts +7 -2
- package/build/src/types/config.d.ts +1 -1
- package/build/src/types/converter.d.ts +31 -28
- package/build/src/types/mixin.d.ts +3 -3
- package/build/src/types/regenerate.d.ts +5 -0
- package/build/src/types/regenerate.js +1 -0
- package/build/src/types/service.d.ts +3 -2
- package/build/src/utils/helpers.d.ts +1 -0
- package/build/src/utils/helpers.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
This package is currently development and will replace [attachment-advanced](https://github.com/batosai/attachment-advanced) for AdonisJS 6.
|
|
4
4
|
|
|
5
|
-
Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit)
|
|
6
|
-
|
|
7
5
|
## Links
|
|
8
6
|
|
|
9
7
|
[View documentation](https://adonis-attachment.jrmc.dev/)
|
|
@@ -12,7 +10,7 @@ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-
|
|
|
12
10
|
|
|
13
11
|
[Discord](https://discord.gg/89eMn2vB)
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit)
|
|
16
14
|
|
|
17
15
|
## Roadmap
|
|
18
16
|
|
|
@@ -21,9 +19,11 @@ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-
|
|
|
21
19
|
- [x] attachment file by path
|
|
22
20
|
- [x] attachment file by url
|
|
23
21
|
- [x] attachment file by stream
|
|
22
|
+
- [x] attachment files
|
|
24
23
|
- [x] save meta data
|
|
25
24
|
- [x] variantes
|
|
26
25
|
- [x] images
|
|
26
|
+
- [x] [blurhash](https://blurha.sh/)
|
|
27
27
|
- [x] documents thumbnail
|
|
28
28
|
- [x] videos thumbnail
|
|
29
29
|
- [ ] command regenerate
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import attachmentManager from './services/main.js';
|
|
2
|
+
import RegenerateService from './services/regenerate_service.js';
|
|
2
3
|
export { configure } from './configure.js';
|
|
3
4
|
export { Attachment } from './src/attachments/attachment.js';
|
|
4
5
|
export { attachment } from './src/decorators/attachment.js';
|
|
@@ -7,4 +8,5 @@ export { defineConfig } from './src/define_config.js';
|
|
|
7
8
|
export { Attachmentable } from './src/mixins/attachmentable.js';
|
|
8
9
|
export * as errors from './src/errors.js';
|
|
9
10
|
export { attachmentManager };
|
|
11
|
+
export { RegenerateService };
|
|
10
12
|
export { type AttachmentVariants } from './src/types/config.js';
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import attachmentManager from './services/main.js';
|
|
2
|
+
import RegenerateService from './services/regenerate_service.js';
|
|
2
3
|
export { configure } from './configure.js';
|
|
3
4
|
export { Attachment } from './src/attachments/attachment.js';
|
|
4
5
|
export { attachment } from './src/decorators/attachment.js';
|
|
@@ -7,3 +8,4 @@ export { defineConfig } from './src/define_config.js';
|
|
|
7
8
|
export { Attachmentable } from './src/mixins/attachmentable.js';
|
|
8
9
|
export * as errors from './src/errors.js';
|
|
9
10
|
export { attachmentManager };
|
|
11
|
+
export { RegenerateService };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LucidRow, LucidModel } from '@adonisjs/lucid/types/model';
|
|
2
|
+
import type { RegenerateOptions } from '../src/types/regenerate.js';
|
|
3
|
+
export default class RegenerateService {
|
|
4
|
+
#private;
|
|
5
|
+
model(Model: LucidModel, options?: RegenerateOptions): this;
|
|
6
|
+
row(row: LucidRow, options?: RegenerateOptions): this;
|
|
7
|
+
run(): Promise<void | void[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Record from '../src/services/record_with_attachment.js';
|
|
2
|
+
export default class RegenerateService {
|
|
3
|
+
#Model;
|
|
4
|
+
#row;
|
|
5
|
+
#options;
|
|
6
|
+
model(Model, options = {}) {
|
|
7
|
+
this.#Model = Model;
|
|
8
|
+
this.#options = options;
|
|
9
|
+
return this;
|
|
10
|
+
}
|
|
11
|
+
row(row, options = {}) {
|
|
12
|
+
this.#row = row;
|
|
13
|
+
this.#options = options;
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
async run() {
|
|
17
|
+
if (this.#row) {
|
|
18
|
+
const record = new Record(this.#row);
|
|
19
|
+
return record.regenerateVariants(this.#options);
|
|
20
|
+
}
|
|
21
|
+
else if (this.#Model) {
|
|
22
|
+
const entities = await this.#Model.all();
|
|
23
|
+
return Promise.all(entities.map(async (entity) => {
|
|
24
|
+
const record = new Record(entity);
|
|
25
|
+
return record.regenerateVariants(this.#options);
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
|
|
8
8
|
import type { MultipartFile } from '@adonisjs/core/bodyparser';
|
|
9
9
|
import type { AttachmentAttributes, AttachmentBase, Attachment as AttachmentType } from './types/attachment.js';
|
|
10
|
+
import type { ResolvedAttachmentConfig } from './define_config.js';
|
|
10
11
|
import { DeferQueue } from '@poppinss/defer';
|
|
11
12
|
import Converter from './converters/converter.js';
|
|
12
|
-
import { ResolvedAttachmentConfig } from './define_config.js';
|
|
13
13
|
export declare class AttachmentManager<KnownConverters extends Record<string, Converter>> {
|
|
14
14
|
#private;
|
|
15
15
|
queue: DeferQueue;
|
|
16
16
|
constructor(config: ResolvedAttachmentConfig<KnownConverters>, drive: DriveService);
|
|
17
|
-
getConfig(): ResolvedAttachmentConfig<KnownConverters>;
|
|
18
17
|
createFromDbResponse(response?: string | JSON): AttachmentType | null;
|
|
19
18
|
createFromFile(input: MultipartFile): Promise<AttachmentType>;
|
|
20
19
|
createFromFiles(inputs: MultipartFile[]): Promise<(AttachmentBase & {
|
|
@@ -34,6 +33,6 @@ export declare class AttachmentManager<KnownConverters extends Record<string, Co
|
|
|
34
33
|
getConverter(key: string): Promise<void | Converter>;
|
|
35
34
|
computeUrl(attachment: AttachmentType | AttachmentBase, signedUrlOptions?: SignedURLOptions): Promise<void>;
|
|
36
35
|
preComputeUrl(attachment: AttachmentType): Promise<void>;
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
write(attachment: AttachmentBase): Promise<void>;
|
|
37
|
+
remove(attachment: AttachmentBase): Promise<void>;
|
|
39
38
|
}
|
|
@@ -23,9 +23,6 @@ export class AttachmentManager {
|
|
|
23
23
|
const concurrency = this.#config.queue?.concurrency || 1;
|
|
24
24
|
this.queue = new DeferQueue({ concurrency });
|
|
25
25
|
}
|
|
26
|
-
getConfig() {
|
|
27
|
-
return this.#config;
|
|
28
|
-
}
|
|
29
26
|
createFromDbResponse(response) {
|
|
30
27
|
if (response === null) {
|
|
31
28
|
return null;
|
|
@@ -125,7 +122,7 @@ export class AttachmentManager {
|
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
|
-
async
|
|
125
|
+
async write(attachment) {
|
|
129
126
|
const destinationPath = attachment.path;
|
|
130
127
|
if (attachment.options?.meta) {
|
|
131
128
|
attachment.meta = await ExifAdapter.exif(attachment.input, this.#config);
|
|
@@ -140,7 +137,7 @@ export class AttachmentManager {
|
|
|
140
137
|
await attachment.getDisk().copyFromFs(attachment.input, destinationPath);
|
|
141
138
|
}
|
|
142
139
|
}
|
|
143
|
-
async
|
|
140
|
+
async remove(attachment) {
|
|
144
141
|
if (attachment.path) {
|
|
145
142
|
await attachment.getDisk().delete(attachment.path);
|
|
146
143
|
if (attachment instanceof Attachment) {
|
|
@@ -45,7 +45,7 @@ export class Attachment extends AttachmentBase {
|
|
|
45
45
|
const attributes = {
|
|
46
46
|
...meta,
|
|
47
47
|
key,
|
|
48
|
-
folder: path.join(this.
|
|
48
|
+
folder: path.join(this.folder, 'variants', this.name),
|
|
49
49
|
};
|
|
50
50
|
const variant = new Variant(this.drive, attributes, input);
|
|
51
51
|
variant.setOptions(this.options);
|
|
@@ -58,17 +58,20 @@ export class Attachment extends AttachmentBase {
|
|
|
58
58
|
getVariant(variantName) {
|
|
59
59
|
return this.variants?.find((v) => v.key === variantName);
|
|
60
60
|
}
|
|
61
|
-
getUrl(variantName) {
|
|
61
|
+
async getUrl(variantName) {
|
|
62
62
|
if (variantName) {
|
|
63
63
|
const variant = this.getVariant(variantName);
|
|
64
64
|
if (variant) {
|
|
65
65
|
variant.setOptions(this.options);
|
|
66
|
-
|
|
66
|
+
const url = await variant.getUrl();
|
|
67
|
+
if (url) {
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
return super.getUrl();
|
|
70
73
|
}
|
|
71
|
-
getSignedUrl(variantNameOrOptions, signedUrlOptions) {
|
|
74
|
+
async getSignedUrl(variantNameOrOptions, signedUrlOptions) {
|
|
72
75
|
let variantName;
|
|
73
76
|
let options = signedUrlOptions;
|
|
74
77
|
if (typeof variantNameOrOptions === 'string') {
|
|
@@ -81,7 +84,10 @@ export class Attachment extends AttachmentBase {
|
|
|
81
84
|
const variant = this.getVariant(variantName);
|
|
82
85
|
if (variant) {
|
|
83
86
|
variant.setOptions(this.options);
|
|
84
|
-
|
|
87
|
+
const url = variant.getSignedUrl(options);
|
|
88
|
+
if (url) {
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
return super.getSignedUrl(options);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
+
import type { LucidRow } from '@adonisjs/lucid/types/model';
|
|
7
8
|
import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
|
|
8
9
|
import type { LucidOptions, AttachmentBaseAttributes, AttachmentBase as AttachmentBaseInterface } from '../types/attachment.js';
|
|
9
10
|
import type { Exif, Input } from '../types/input.js';
|
|
@@ -17,7 +18,7 @@ export declare class AttachmentBase implements AttachmentBaseInterface {
|
|
|
17
18
|
meta?: Exif;
|
|
18
19
|
originalPath?: string;
|
|
19
20
|
url?: string;
|
|
20
|
-
options
|
|
21
|
+
options: LucidOptions;
|
|
21
22
|
constructor(drive: DriveService, attributes: AttachmentBaseAttributes, input?: Input);
|
|
22
23
|
/**
|
|
23
24
|
* Getters
|
|
@@ -29,9 +30,11 @@ export declare class AttachmentBase implements AttachmentBaseInterface {
|
|
|
29
30
|
* Methods
|
|
30
31
|
*/
|
|
31
32
|
getDisk(): import("flydrive").Disk;
|
|
33
|
+
getStream(): Promise<import("stream").Readable>;
|
|
32
34
|
getUrl(): Promise<string>;
|
|
33
35
|
getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
|
|
34
36
|
setOptions(options: LucidOptions): this;
|
|
37
|
+
makeFolder(record?: LucidRow): this;
|
|
35
38
|
/**
|
|
36
39
|
*
|
|
37
40
|
*/
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
+
import string from '@adonisjs/core/helpers/string';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { cuid } from '@adonisjs/core/helpers';
|
|
9
10
|
import { defaultOptionsDecorator } from '../utils/default_values.js';
|
|
11
|
+
import { extractPathParameters } from '../utils/helpers.js';
|
|
10
12
|
export class AttachmentBase {
|
|
11
13
|
drive;
|
|
12
14
|
input;
|
|
@@ -27,6 +29,7 @@ export class AttachmentBase {
|
|
|
27
29
|
this.mimeType = attributes.mimeType;
|
|
28
30
|
this.originalPath = attributes.path;
|
|
29
31
|
this.#folder = attributes.folder;
|
|
32
|
+
this.setOptions({ folder: attributes.folder });
|
|
30
33
|
if (attributes.name) {
|
|
31
34
|
this.#name = attributes.name;
|
|
32
35
|
}
|
|
@@ -43,12 +46,20 @@ export class AttachmentBase {
|
|
|
43
46
|
return this.#name;
|
|
44
47
|
}
|
|
45
48
|
get folder() {
|
|
46
|
-
if (this
|
|
47
|
-
return this
|
|
49
|
+
if (this.#folder) {
|
|
50
|
+
return this.#folder;
|
|
51
|
+
}
|
|
52
|
+
if (typeof this.options.folder === 'string') {
|
|
53
|
+
const parameters = extractPathParameters(this.options.folder);
|
|
54
|
+
if (!parameters.length) {
|
|
55
|
+
return this.options.folder;
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
|
-
return this.#folder;
|
|
50
58
|
}
|
|
51
59
|
get path() {
|
|
60
|
+
if (!this.folder && this.originalPath) {
|
|
61
|
+
return this.originalPath;
|
|
62
|
+
}
|
|
52
63
|
return path.join(this.folder, this.name);
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
@@ -57,6 +68,9 @@ export class AttachmentBase {
|
|
|
57
68
|
getDisk() {
|
|
58
69
|
return this.drive.use(this.options?.disk);
|
|
59
70
|
}
|
|
71
|
+
getStream() {
|
|
72
|
+
return this.getDisk().getStream(this.path);
|
|
73
|
+
}
|
|
60
74
|
getUrl() {
|
|
61
75
|
return this.getDisk().getUrl(this.path);
|
|
62
76
|
}
|
|
@@ -70,6 +84,27 @@ export class AttachmentBase {
|
|
|
70
84
|
};
|
|
71
85
|
return this;
|
|
72
86
|
}
|
|
87
|
+
makeFolder(record) {
|
|
88
|
+
if (typeof this.options.folder === 'function' && record) {
|
|
89
|
+
this.#folder = this.options.folder(record);
|
|
90
|
+
}
|
|
91
|
+
else if (this.options.folder) {
|
|
92
|
+
this.#folder = this.options.folder;
|
|
93
|
+
}
|
|
94
|
+
if (this.#folder && record) {
|
|
95
|
+
const parameters = extractPathParameters(this.#folder);
|
|
96
|
+
if (parameters) {
|
|
97
|
+
parameters.forEach((parameter) => {
|
|
98
|
+
const attribute = record.$attributes[parameter];
|
|
99
|
+
if (typeof attribute === 'string') {
|
|
100
|
+
const name = string.slug(string.noCase(string.escapeHTML(attribute.toLowerCase())));
|
|
101
|
+
this.#folder = this.#folder?.replace(`:${parameter}`, name);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
73
108
|
/**
|
|
74
109
|
*
|
|
75
110
|
*/
|
|
@@ -8,11 +8,13 @@ import type { DriveService } from '@adonisjs/drive/types';
|
|
|
8
8
|
import type { VariantAttributes, Variant as VariantInterface } from '../types/attachment.js';
|
|
9
9
|
import type { Input } from '../types/input.js';
|
|
10
10
|
import { AttachmentBase } from './attachment_base.js';
|
|
11
|
+
import { BlurhashOptions } from '../types/converter.js';
|
|
11
12
|
export declare class Variant extends AttachmentBase implements VariantInterface {
|
|
12
13
|
#private;
|
|
13
14
|
key: string;
|
|
14
15
|
blurhash?: string;
|
|
15
16
|
constructor(drive: DriveService, attributes: VariantAttributes, input?: Input);
|
|
17
|
+
generateBlurhash(options?: BlurhashOptions): Promise<void>;
|
|
16
18
|
/**
|
|
17
19
|
* Getters
|
|
18
20
|
*/
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
7
|
import { AttachmentBase } from './attachment_base.js';
|
|
8
|
+
import { imageToBlurhash } from '../utils/helpers.js';
|
|
8
9
|
export class Variant extends AttachmentBase {
|
|
9
10
|
key;
|
|
10
11
|
#folder;
|
|
@@ -15,6 +16,9 @@ export class Variant extends AttachmentBase {
|
|
|
15
16
|
this.#folder = attributes.folder;
|
|
16
17
|
this.blurhash = attributes.blurhash;
|
|
17
18
|
}
|
|
19
|
+
async generateBlurhash(options) {
|
|
20
|
+
this.blurhash = await imageToBlurhash(this.input, options);
|
|
21
|
+
}
|
|
18
22
|
/**
|
|
19
23
|
* Getters
|
|
20
24
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConverterInitializeAttributes } from './types/converter.js';
|
|
2
2
|
export declare class ConverterManager {
|
|
3
3
|
#private;
|
|
4
|
-
constructor({ record, attributeName, options }: ConverterInitializeAttributes);
|
|
5
|
-
|
|
4
|
+
constructor({ record, attributeName, options, filters }: ConverterInitializeAttributes);
|
|
5
|
+
run(): Promise<void>;
|
|
6
6
|
}
|
|
@@ -3,71 +3,93 @@ import string from '@adonisjs/core/helpers/string';
|
|
|
3
3
|
import db from '@adonisjs/lucid/services/db';
|
|
4
4
|
import attachmentManager from '../services/main.js';
|
|
5
5
|
import * as errors from './errors.js';
|
|
6
|
-
import {
|
|
6
|
+
import { streamToTempFile } from './utils/helpers.js';
|
|
7
7
|
export class ConverterManager {
|
|
8
8
|
#record;
|
|
9
9
|
#attributeName;
|
|
10
10
|
#options;
|
|
11
|
-
|
|
11
|
+
#filters;
|
|
12
|
+
constructor({ record, attributeName, options, filters }) {
|
|
12
13
|
this.#record = record;
|
|
13
14
|
this.#attributeName = attributeName;
|
|
14
15
|
this.#options = options;
|
|
16
|
+
this.#filters = filters;
|
|
15
17
|
}
|
|
16
|
-
async
|
|
17
|
-
|
|
18
|
+
async run() {
|
|
19
|
+
const attachments = this.#record.getAttachments({
|
|
18
20
|
attributeName: this.#attributeName,
|
|
19
21
|
});
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if ((typeof converter.options.blurhash !== 'boolean' &&
|
|
39
|
-
converter.options.blurhash.enabled === true) ||
|
|
40
|
-
converter.options.blurhash === true) {
|
|
41
|
-
try {
|
|
42
|
-
const options = typeof converter.options.blurhash !== 'boolean'
|
|
43
|
-
? converter.options.blurhash
|
|
44
|
-
: undefined;
|
|
45
|
-
variant.blurhash = await imageToBlurhash(variant.input, options);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
logger.error(error.message);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
await attachmentManager.save(variant);
|
|
53
|
-
}
|
|
22
|
+
const variants = [];
|
|
23
|
+
await this.#purge(attachments);
|
|
24
|
+
if (!attachments || !this.#options.variants || !this.#options.variants.length) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for await (const key of this.#options.variants) {
|
|
28
|
+
if (this.#filters?.variants !== undefined && !this.#filters?.variants?.includes(key)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const converter = (await attachmentManager.getConverter(key));
|
|
32
|
+
if (converter) {
|
|
33
|
+
for await (const attachment of attachments) {
|
|
34
|
+
const variant = await this.#generate({
|
|
35
|
+
key,
|
|
36
|
+
attachment,
|
|
37
|
+
converter
|
|
38
|
+
});
|
|
39
|
+
variants.push(variant);
|
|
54
40
|
}
|
|
55
41
|
}
|
|
56
42
|
}
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
return this.#commit(attachments, () => {
|
|
44
|
+
for (let i = 0; i < variants.length; i++) {
|
|
45
|
+
attachmentManager.remove(variants[i]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async #generate({ key, attachment, converter }) {
|
|
50
|
+
let input = attachment.input;
|
|
51
|
+
if (!input) {
|
|
52
|
+
input = await streamToTempFile(await attachment.getStream());
|
|
59
53
|
}
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const output = await converter.handle({
|
|
55
|
+
input,
|
|
56
|
+
options: converter.options,
|
|
57
|
+
});
|
|
58
|
+
if (output === undefined) {
|
|
59
|
+
throw new errors.E_CANNOT_PATH_BY_CONVERTER();
|
|
62
60
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
const variant = await attachment.createVariant(key, output);
|
|
62
|
+
if (converter.options.blurhash) {
|
|
63
|
+
if ((typeof converter.options.blurhash !== 'boolean' &&
|
|
64
|
+
converter.options.blurhash.enabled === true) ||
|
|
65
|
+
converter.options.blurhash === true) {
|
|
66
|
+
try {
|
|
67
|
+
const options = typeof converter.options.blurhash !== 'boolean'
|
|
68
|
+
? converter.options.blurhash
|
|
69
|
+
: undefined;
|
|
70
|
+
await variant.generateBlurhash(options);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.error(error.message);
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
|
-
}
|
|
76
|
+
}
|
|
77
|
+
await attachmentManager.write(variant);
|
|
78
|
+
return variant;
|
|
79
|
+
}
|
|
80
|
+
async #commit(attachments, rollback) {
|
|
81
|
+
const Model = this.#record.row.constructor;
|
|
82
|
+
const id = this.#record.row.$attributes['id'];
|
|
83
|
+
const data = {};
|
|
84
|
+
const index = string.snakeCase(this.#attributeName);
|
|
85
|
+
if (Array.isArray(this.#record.row.$original[this.#attributeName])) {
|
|
86
|
+
data[index] = JSON.stringify(attachments.map((att) => att.toObject()));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
data[index] = JSON.stringify(attachments[0].toObject());
|
|
90
|
+
}
|
|
91
|
+
const trx = await db.transaction();
|
|
92
|
+
trx.after('rollback', rollback);
|
|
71
93
|
try {
|
|
72
94
|
await trx.query().from(Model.table).where('id', id).update(data);
|
|
73
95
|
return trx.commit();
|
|
@@ -76,4 +98,22 @@ export class ConverterManager {
|
|
|
76
98
|
return trx.rollback();
|
|
77
99
|
}
|
|
78
100
|
}
|
|
101
|
+
async #purge(attachments) {
|
|
102
|
+
return Promise.all(attachments.map(async (attachment) => {
|
|
103
|
+
if (attachment.variants) {
|
|
104
|
+
await Promise.all(attachment.variants.map(async (variant) => {
|
|
105
|
+
if (this.#filters?.variants !== undefined && !this.#filters?.variants?.includes(variant.key)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
return attachmentManager.remove(variant);
|
|
109
|
+
}));
|
|
110
|
+
if (this.#filters?.variants !== undefined) {
|
|
111
|
+
attachment.variants = await attachment.variants.filter((variant) => !this.#filters?.variants?.includes(variant.key));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
attachment.variants = [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
79
119
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
7
|
import type { ConverterAttributes } from '../types/converter.js';
|
|
8
|
+
import type { Input } from '../types/input.js';
|
|
8
9
|
import Converter from './converter.js';
|
|
9
10
|
export default class AutodetectConverter extends Converter {
|
|
10
|
-
handle({ input, options }: ConverterAttributes): Promise<
|
|
11
|
+
handle({ input, options }: ConverterAttributes): Promise<Input | undefined>;
|
|
11
12
|
}
|
|
@@ -18,17 +18,15 @@ export default class AutodetectConverter extends Converter {
|
|
|
18
18
|
else {
|
|
19
19
|
fileType = await fileTypeFromFile(input);
|
|
20
20
|
}
|
|
21
|
-
if (fileType?.mime.includes('
|
|
22
|
-
converter = new ImageConverter(options, this.binPaths);
|
|
23
|
-
}
|
|
24
|
-
else if (fileType?.mime.includes('video')) {
|
|
21
|
+
if (fileType?.mime.includes('video')) {
|
|
25
22
|
converter = new VideoThumnailConverter(options, this.binPaths);
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
input,
|
|
30
|
-
options,
|
|
31
|
-
});
|
|
24
|
+
else {
|
|
25
|
+
converter = new ImageConverter(options, this.binPaths);
|
|
32
26
|
}
|
|
27
|
+
return converter.handle({
|
|
28
|
+
input,
|
|
29
|
+
options,
|
|
30
|
+
});
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
7
|
import type { BinPaths } from '../types/config.js';
|
|
8
|
-
import type { Converter as ConverterInterface, ConverterOptions } from '../types/converter.js';
|
|
8
|
+
import type { Converter as ConverterInterface, ConverterOptions, ConverterAttributes } from '../types/converter.js';
|
|
9
|
+
import type { Input } from '../types/input.js';
|
|
9
10
|
export default class Converter implements ConverterInterface {
|
|
10
11
|
options?: ConverterOptions;
|
|
11
12
|
binPaths?: BinPaths;
|
|
12
13
|
constructor(options?: ConverterOptions, binPaths?: BinPaths);
|
|
14
|
+
handle({ input }: ConverterAttributes): Promise<Input | undefined>;
|
|
13
15
|
}
|
|
@@ -8,6 +8,6 @@ import type { ConverterAttributes } from '../types/converter.js';
|
|
|
8
8
|
import type { Input } from '../types/input.js';
|
|
9
9
|
import Converter from './converter.js';
|
|
10
10
|
export default class VideoThumbnailConvert extends Converter {
|
|
11
|
-
handle({ input, options }: ConverterAttributes): Promise<
|
|
12
|
-
videoToImage(ffmpeg: Function, input: Input): Promise<string |
|
|
11
|
+
handle({ input, options }: ConverterAttributes): Promise<Input | undefined>;
|
|
12
|
+
videoToImage(ffmpeg: Function, input: Input): Promise<string | undefined>;
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@ export default class VideoThumbnailConvert extends Converter {
|
|
|
16
16
|
const filePath = await this.videoToImage(ffmpeg, input);
|
|
17
17
|
if (options && filePath) {
|
|
18
18
|
const converter = new ImageConverter();
|
|
19
|
-
return
|
|
19
|
+
return converter.handle({
|
|
20
20
|
input: filePath,
|
|
21
21
|
options,
|
|
22
22
|
});
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { LucidModel } from '@adonisjs/lucid/types/model';
|
|
8
8
|
import type { LucidOptions } from '../types/attachment.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { AttributeOfRowWithAttachment } from '../types/mixin.js';
|
|
10
10
|
export declare const bootModel: (model: LucidModel & {
|
|
11
|
-
$attachments:
|
|
11
|
+
$attachments: AttributeOfRowWithAttachment;
|
|
12
12
|
}) => void;
|
|
13
13
|
export declare const attachment: (options?: LucidOptions) => (target: any, attributeName: string) => void;
|
|
14
14
|
export declare const attachments: (options?: LucidOptions) => (target: any, attributeName: string) => void;
|
|
@@ -16,8 +16,8 @@ export type ResolvedAttachmentConfig<KnownConverters extends Record<string, Conv
|
|
|
16
16
|
rename?: boolean;
|
|
17
17
|
preComputeUrl?: boolean;
|
|
18
18
|
converters?: {
|
|
19
|
-
[K in keyof KnownConverters]:
|
|
19
|
+
[K in keyof KnownConverters]: Converter;
|
|
20
20
|
};
|
|
21
21
|
queue?: Queue;
|
|
22
22
|
};
|
|
23
|
-
export declare function defineConfig<KnownConverter extends Record<string, ConverterConfig>>(config: AttachmentConfig<KnownConverter>): ConfigProvider<ResolvedAttachmentConfig<
|
|
23
|
+
export declare function defineConfig<KnownConverter extends Record<string, ConverterConfig>>(config: AttachmentConfig<KnownConverter>): ConfigProvider<ResolvedAttachmentConfig<Record<string, Converter>>>;
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
7
|
import { configProvider } from '@adonisjs/core';
|
|
8
|
-
// export function defineConfig<T extends AttachmentConfig>(config: T): T {
|
|
9
8
|
export function defineConfig(config) {
|
|
10
9
|
return configProvider.create(async (_app) => {
|
|
11
10
|
const convertersList = Object.keys(config.converters || {});
|
|
@@ -14,9 +13,14 @@ export function defineConfig(config) {
|
|
|
14
13
|
for (let converterName of convertersList) {
|
|
15
14
|
const converter = config.converters[converterName];
|
|
16
15
|
const binConfig = config.bin;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
try {
|
|
17
|
+
const { default: value } = await converter.converter();
|
|
18
|
+
const Converter = value;
|
|
19
|
+
converters[converterName] = new Converter(converter.options, binConfig);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(`Failed to load converter ${converterName}:`, error);
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
return {
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { BaseModel } from '@adonisjs/lucid/orm';
|
|
8
8
|
import type { NormalizeConstructor } from '@adonisjs/core/types/helpers';
|
|
9
|
-
import type {
|
|
9
|
+
import type { AttributeOfRowWithAttachment } from '../types/mixin.js';
|
|
10
10
|
type Constructor = NormalizeConstructor<typeof BaseModel>;
|
|
11
11
|
export declare function Attachmentable<T extends Constructor>(superclass: T): {
|
|
12
12
|
new (...args: any[]): {
|
|
13
|
-
$attachments:
|
|
13
|
+
$attachments: AttributeOfRowWithAttachment;
|
|
14
14
|
$attributes: import("@adonisjs/lucid/types/model").ModelObject;
|
|
15
15
|
$extras: import("@adonisjs/lucid/types/model").ModelObject;
|
|
16
16
|
$original: import("@adonisjs/lucid/types/model").ModelObject;
|
|
@@ -43,10 +43,10 @@ export declare function Attachmentable<T extends Constructor>(superclass: T): {
|
|
|
43
43
|
$consumeAdapterResult(adapterResult: import("@adonisjs/lucid/types/model").ModelObject, sideloadAttributes?: import("@adonisjs/lucid/types/model").ModelObject): void;
|
|
44
44
|
$hydrateOriginals(): void;
|
|
45
45
|
fill(value: Partial<{
|
|
46
|
-
$attachments:
|
|
46
|
+
$attachments: AttributeOfRowWithAttachment;
|
|
47
47
|
}>, allowExtraProperties?: boolean): /*elided*/ any;
|
|
48
48
|
merge(value: Partial<{
|
|
49
|
-
$attachments:
|
|
49
|
+
$attachments: AttributeOfRowWithAttachment;
|
|
50
50
|
}>, allowExtraProperties?: boolean): /*elided*/ any;
|
|
51
51
|
isDirty(fields?: "$attachments" | ("$attachments" | undefined)[] | undefined): boolean;
|
|
52
52
|
enableForceUpdate(): /*elided*/ any;
|
|
@@ -14,7 +14,7 @@ import { beforeSave, beforeFind, beforeFetch, beforePaginate, beforeCreate, } fr
|
|
|
14
14
|
import { defaultStateAttributeMixin } from '../utils/default_values.js';
|
|
15
15
|
import logger from '@adonisjs/core/services/logger';
|
|
16
16
|
export function Attachmentable(superclass) {
|
|
17
|
-
class
|
|
17
|
+
class RowWithAttachment extends superclass {
|
|
18
18
|
$attachments = structuredClone(defaultStateAttributeMixin);
|
|
19
19
|
static async warn() {
|
|
20
20
|
logger.warn(`The "Attachmentable" mixin is deprecated and may be removed in a future version.`);
|
|
@@ -26,6 +26,6 @@ export function Attachmentable(superclass) {
|
|
|
26
26
|
beforeFetch(),
|
|
27
27
|
beforePaginate(),
|
|
28
28
|
beforeSave()
|
|
29
|
-
],
|
|
30
|
-
return
|
|
29
|
+
], RowWithAttachment, "warn", null);
|
|
30
|
+
return RowWithAttachment;
|
|
31
31
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RowWithAttachment } from '../types/mixin.js';
|
|
2
2
|
import type { Attachment as AttachmentType } from '../types/attachment.js';
|
|
3
3
|
import type { Record as RecordImplementation } from '../types/service.js';
|
|
4
|
+
import type { RegenerateOptions } from '../types/regenerate.js';
|
|
4
5
|
export default class Record implements RecordImplementation {
|
|
5
6
|
#private;
|
|
6
|
-
constructor(
|
|
7
|
+
constructor(row: RowWithAttachment);
|
|
7
8
|
/**
|
|
8
9
|
* During commit, we should cleanup the old detached files
|
|
9
10
|
*/
|
|
@@ -18,11 +19,13 @@ export default class Record implements RecordImplementation {
|
|
|
18
19
|
}): Promise<void>;
|
|
19
20
|
preComputeUrl(): Promise<void>;
|
|
20
21
|
generateVariants(): Promise<void>;
|
|
22
|
+
regenerateVariants(options?: RegenerateOptions): Promise<void>;
|
|
21
23
|
detach(): Promise<PromiseSettledResult<void>[]>;
|
|
22
24
|
detachAll(): Promise<PromiseSettledResult<void>[]>;
|
|
23
|
-
get
|
|
25
|
+
get row(): RowWithAttachment;
|
|
24
26
|
getAttachments(options: {
|
|
25
27
|
attributeName: string;
|
|
26
28
|
requiredOriginal?: boolean;
|
|
29
|
+
requiredDirty?: boolean;
|
|
27
30
|
}): AttachmentType[];
|
|
28
31
|
}
|
|
@@ -5,32 +5,32 @@ import { optionsSym } from '../utils/symbols.js';
|
|
|
5
5
|
import { ConverterManager } from '../converter_manager.js';
|
|
6
6
|
import { E_CANNOT_CREATE_VARIANT } from '../errors.js';
|
|
7
7
|
export default class Record {
|
|
8
|
-
#
|
|
9
|
-
constructor(
|
|
10
|
-
this.#
|
|
11
|
-
if (!this.#
|
|
8
|
+
#row;
|
|
9
|
+
constructor(row) {
|
|
10
|
+
this.#row = row;
|
|
11
|
+
if (!this.#row.$attachments) {
|
|
12
12
|
/**
|
|
13
13
|
* Empty previous $attachments
|
|
14
14
|
*/
|
|
15
|
-
this.#
|
|
15
|
+
this.#row.$attachments = structuredClone(defaultStateAttributeMixin);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* During commit, we should cleanup the old detached files
|
|
20
20
|
*/
|
|
21
21
|
async commit() {
|
|
22
|
-
await Promise.allSettled(this.#
|
|
22
|
+
await Promise.allSettled(this.#row.$attachments.detached.map((attachment) => attachmentManager.remove(attachment)));
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* During rollback we should remove the attached files.
|
|
26
26
|
*/
|
|
27
27
|
async rollback() {
|
|
28
|
-
await Promise.allSettled(this.#
|
|
28
|
+
await Promise.allSettled(this.#row.$attachments.attached.map((attachment) => attachmentManager.remove(attachment)));
|
|
29
29
|
}
|
|
30
30
|
async persist() {
|
|
31
31
|
const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
|
|
32
32
|
/**
|
|
33
|
-
* Persist attachments before saving the
|
|
33
|
+
* Persist attachments before saving the row to the database. This
|
|
34
34
|
* way if file saving fails we will not write anything to the
|
|
35
35
|
* database
|
|
36
36
|
*/
|
|
@@ -47,7 +47,7 @@ export default class Record {
|
|
|
47
47
|
/**
|
|
48
48
|
* memorise attribute name for generate variants
|
|
49
49
|
*/
|
|
50
|
-
this.#
|
|
50
|
+
this.#row.$attachments.dirtied.push(name);
|
|
51
51
|
for (let i = 0; i < newAttachments.length; i++) {
|
|
52
52
|
if (originalAttachments.includes(newAttachments[i])) {
|
|
53
53
|
continue;
|
|
@@ -57,22 +57,22 @@ export default class Record {
|
|
|
57
57
|
* file.
|
|
58
58
|
*/
|
|
59
59
|
if (newAttachments[i]) {
|
|
60
|
-
newAttachments[i].setOptions(options);
|
|
61
|
-
this.#
|
|
60
|
+
newAttachments[i].setOptions(options).makeFolder(this.#row);
|
|
61
|
+
this.#row.$attachments.attached.push(newAttachments[i]);
|
|
62
62
|
/**
|
|
63
63
|
* Also write the file to the disk right away
|
|
64
64
|
*/
|
|
65
|
-
await attachmentManager.
|
|
65
|
+
await attachmentManager.write(newAttachments[i]);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
70
|
async transaction(options = { enabledRollback: true }) {
|
|
71
71
|
try {
|
|
72
|
-
if (this.#
|
|
73
|
-
this.#
|
|
72
|
+
if (this.#row.$trx) {
|
|
73
|
+
this.#row.$trx.after('commit', () => this.commit());
|
|
74
74
|
if (options.enabledRollback) {
|
|
75
|
-
this.#
|
|
75
|
+
this.#row.$trx.after('rollback', () => this.rollback());
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
@@ -90,7 +90,7 @@ export default class Record {
|
|
|
90
90
|
const attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
|
|
91
91
|
await Promise.all(attachmentAttributeNames.map(async (name) => {
|
|
92
92
|
const options = this.#getOptionsByAttributeName(name);
|
|
93
|
-
if (this.#
|
|
93
|
+
if (this.#row.$attributes[name]) {
|
|
94
94
|
const attachments = this.#getAttachmentsByAttributeName(name);
|
|
95
95
|
for (let i = 0; i < attachments.length; i++) {
|
|
96
96
|
attachments[i].setOptions(options);
|
|
@@ -100,8 +100,8 @@ export default class Record {
|
|
|
100
100
|
}));
|
|
101
101
|
}
|
|
102
102
|
async generateVariants() {
|
|
103
|
-
/* this.#
|
|
104
|
-
const attachmentAttributeNames = this.#
|
|
103
|
+
/* this.#row.$dirty is not avalable in afterSave hooks */
|
|
104
|
+
const attachmentAttributeNames = this.#row.$attachments.dirtied;
|
|
105
105
|
/**
|
|
106
106
|
* For all properties Attachment
|
|
107
107
|
* Launch async generation variants
|
|
@@ -109,7 +109,7 @@ export default class Record {
|
|
|
109
109
|
for await (const name of attachmentAttributeNames) {
|
|
110
110
|
const record = this;
|
|
111
111
|
attachmentManager.queue.push({
|
|
112
|
-
name: `${this.#
|
|
112
|
+
name: `${this.#row.constructor.name}-${name}`,
|
|
113
113
|
async run() {
|
|
114
114
|
try {
|
|
115
115
|
const converterManager = new ConverterManager({
|
|
@@ -117,7 +117,38 @@ export default class Record {
|
|
|
117
117
|
attributeName: name,
|
|
118
118
|
options: record.#getOptionsByAttributeName(name),
|
|
119
119
|
});
|
|
120
|
-
await converterManager.
|
|
120
|
+
await converterManager.run();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new E_CANNOT_CREATE_VARIANT([err.message]);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async regenerateVariants(options = {}) {
|
|
130
|
+
let attachmentAttributeNames;
|
|
131
|
+
if (options.attributes?.length) {
|
|
132
|
+
attachmentAttributeNames = options.attributes;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
attachmentAttributeNames = this.#getAttributeNamesOfAttachment();
|
|
136
|
+
}
|
|
137
|
+
for await (const name of attachmentAttributeNames) {
|
|
138
|
+
const record = this;
|
|
139
|
+
attachmentManager.queue.push({
|
|
140
|
+
name: `${this.#row.constructor.name}-${name}`,
|
|
141
|
+
async run() {
|
|
142
|
+
try {
|
|
143
|
+
const converterManager = new ConverterManager({
|
|
144
|
+
record,
|
|
145
|
+
attributeName: name,
|
|
146
|
+
options: record.#getOptionsByAttributeName(name),
|
|
147
|
+
filters: {
|
|
148
|
+
variants: options.variants
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
await converterManager.run();
|
|
121
152
|
}
|
|
122
153
|
catch (err) {
|
|
123
154
|
throw new E_CANNOT_CREATE_VARIANT([err.message]);
|
|
@@ -134,7 +165,7 @@ export default class Record {
|
|
|
134
165
|
return Promise.allSettled(attachmentAttributeNames.map((name) => {
|
|
135
166
|
let attachments = [];
|
|
136
167
|
const options = this.#getOptionsByAttributeName(name);
|
|
137
|
-
if (this.#
|
|
168
|
+
if (this.#row.$dirty[name] === null) {
|
|
138
169
|
attachments = this.#getOriginalAttachmentsByAttributeName(name);
|
|
139
170
|
}
|
|
140
171
|
else {
|
|
@@ -158,7 +189,7 @@ export default class Record {
|
|
|
158
189
|
}
|
|
159
190
|
for (let i = 0; i < attachments.length; i++) {
|
|
160
191
|
attachments[i].setOptions(options);
|
|
161
|
-
this.#
|
|
192
|
+
this.#row.$attachments.detached.push(attachments[i]);
|
|
162
193
|
}
|
|
163
194
|
}));
|
|
164
195
|
}
|
|
@@ -172,53 +203,68 @@ export default class Record {
|
|
|
172
203
|
const attachments = this.#getAttachmentsByAttributeName(name);
|
|
173
204
|
for (let i = 0; i < attachments.length; i++) {
|
|
174
205
|
attachments[i].setOptions(options);
|
|
175
|
-
this.#
|
|
206
|
+
this.#row.$attachments.detached.push(attachments[i]);
|
|
176
207
|
}
|
|
177
208
|
}));
|
|
178
209
|
}
|
|
179
|
-
get
|
|
180
|
-
return this.#
|
|
210
|
+
get row() {
|
|
211
|
+
return this.#row;
|
|
181
212
|
}
|
|
182
213
|
getAttachments(options) {
|
|
214
|
+
let attachments;
|
|
183
215
|
if (options.requiredOriginal) {
|
|
184
|
-
|
|
216
|
+
attachments = this.#getOriginalAttachmentsByAttributeName(options.attributeName);
|
|
217
|
+
}
|
|
218
|
+
else if (options.requiredDirty) {
|
|
219
|
+
attachments = this.#getDirtyAttachmentsByAttributeName(options.attributeName);
|
|
185
220
|
}
|
|
186
221
|
else {
|
|
187
|
-
|
|
222
|
+
attachments = this.#getAttachmentsByAttributeName(options.attributeName);
|
|
188
223
|
}
|
|
224
|
+
const opts = this.#getOptionsByAttributeName(options.attributeName);
|
|
225
|
+
attachments.map((attachment) => attachment.setOptions(opts).makeFolder(this.#row));
|
|
226
|
+
return attachments;
|
|
189
227
|
}
|
|
190
228
|
#getAttachmentsByAttributeName(name) {
|
|
191
|
-
if (Array.isArray(this.#
|
|
192
|
-
return this.#
|
|
229
|
+
if (Array.isArray(this.#row.$attributes[name])) {
|
|
230
|
+
return this.#row.$attributes[name];
|
|
193
231
|
}
|
|
194
|
-
return [this.#
|
|
232
|
+
return [this.#row.$attributes[name]];
|
|
195
233
|
}
|
|
196
234
|
#getOriginalAttachmentsByAttributeName(name) {
|
|
197
|
-
if (Array.isArray(this.#
|
|
198
|
-
return this.#
|
|
235
|
+
if (Array.isArray(this.#row.$original[name])) {
|
|
236
|
+
return this.#row.$original[name];
|
|
237
|
+
}
|
|
238
|
+
return [this.#row.$original[name]];
|
|
239
|
+
}
|
|
240
|
+
#getDirtyAttachmentsByAttributeName(name) {
|
|
241
|
+
if (Array.isArray(this.#row.$dirty[name])) {
|
|
242
|
+
return this.#row.$dirty[name];
|
|
199
243
|
}
|
|
200
|
-
return [this.#
|
|
244
|
+
return [this.#row.$dirty[name]];
|
|
201
245
|
}
|
|
202
246
|
#getOptionsByAttributeName(name) {
|
|
203
|
-
return this.#
|
|
247
|
+
return this.#row.constructor.prototype[optionsSym]?.[name];
|
|
204
248
|
}
|
|
205
249
|
#getAttributeNamesOfAttachment() {
|
|
206
|
-
return Object.keys(this.#
|
|
207
|
-
const value = this.#
|
|
250
|
+
return Object.keys(this.#row.$attributes).filter((name) => {
|
|
251
|
+
const value = this.#row.$attributes[name];
|
|
208
252
|
return (value instanceof Attachment ||
|
|
209
253
|
(Array.isArray(value) && value.every((item) => item instanceof Attachment)));
|
|
210
254
|
});
|
|
211
255
|
}
|
|
212
256
|
#getDirtyAttributeNamesOfAttachment() {
|
|
213
|
-
return Object.keys(this.#
|
|
214
|
-
const dirtyValue = this.#
|
|
215
|
-
const originalValue = this.#
|
|
257
|
+
return Object.keys(this.#row.$dirty).filter((name) => {
|
|
258
|
+
const dirtyValue = this.#row.$dirty[name];
|
|
259
|
+
const originalValue = this.#row.$original[name]; // if dirtyValue is null, check original type
|
|
216
260
|
const isDirtyAttachment = dirtyValue instanceof Attachment ||
|
|
217
261
|
(Array.isArray(dirtyValue) &&
|
|
218
|
-
dirtyValue.
|
|
219
|
-
dirtyValue.
|
|
262
|
+
dirtyValue.length &&
|
|
263
|
+
dirtyValue.every((item) => item instanceof Attachment));
|
|
220
264
|
const isOriginalAttachment = originalValue instanceof Attachment ||
|
|
221
|
-
(Array.isArray(originalValue) &&
|
|
265
|
+
(Array.isArray(originalValue) &&
|
|
266
|
+
originalValue.length &&
|
|
267
|
+
originalValue.every((item) => item instanceof Attachment));
|
|
222
268
|
return isDirtyAttachment || isOriginalAttachment;
|
|
223
269
|
});
|
|
224
270
|
}
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
+
import type { LucidRow } from '@adonisjs/lucid/types/model';
|
|
7
8
|
import type { DriveService } from '@adonisjs/drive/types';
|
|
8
9
|
import type { Exif, Input } from './input.js';
|
|
9
10
|
import type { Disk } from '@adonisjs/drive';
|
|
10
11
|
import type { SignedURLOptions } from '@adonisjs/drive/types';
|
|
11
12
|
import type { AttachmentVariants } from '@jrmc/adonis-attachment';
|
|
13
|
+
import { BlurhashOptions } from './converter.js';
|
|
12
14
|
export type AttachmentBase = {
|
|
13
15
|
drive: DriveService;
|
|
14
16
|
input?: Input;
|
|
@@ -21,8 +23,10 @@ export type AttachmentBase = {
|
|
|
21
23
|
meta?: Exif;
|
|
22
24
|
originalPath?: string;
|
|
23
25
|
url?: string;
|
|
24
|
-
options
|
|
26
|
+
options: LucidOptions;
|
|
27
|
+
makeFolder(record?: LucidRow): void;
|
|
25
28
|
getDisk(): Disk;
|
|
29
|
+
getStream(): Promise<NodeJS.ReadableStream>;
|
|
26
30
|
getUrl(): Promise<string>;
|
|
27
31
|
getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
|
|
28
32
|
setOptions(options: LucidOptions): AttachmentBase;
|
|
@@ -42,11 +46,12 @@ export type Variant = AttachmentBase & {
|
|
|
42
46
|
key: string;
|
|
43
47
|
folder: string;
|
|
44
48
|
blurhash?: string;
|
|
49
|
+
generateBlurhash(options?: BlurhashOptions): void;
|
|
45
50
|
toObject(): VariantAttributes;
|
|
46
51
|
};
|
|
47
52
|
export type LucidOptions = {
|
|
48
53
|
disk?: string;
|
|
49
|
-
folder?: string;
|
|
54
|
+
folder?: string | ((record?: LucidRow) => string);
|
|
50
55
|
preComputeUrl?: boolean;
|
|
51
56
|
variants?: (keyof AttachmentVariants)[];
|
|
52
57
|
rename?: boolean;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
|
|
6
6
|
*/
|
|
7
|
-
import { ConfigProvider } from '@adonisjs/core/types';
|
|
8
7
|
import type { Converter, ConverterOptions } from './converter.js';
|
|
8
|
+
import { ConfigProvider } from '@adonisjs/core/types';
|
|
9
9
|
import { AttachmentManager } from '../attachment_manager.js';
|
|
10
10
|
type ImportConverter = {
|
|
11
11
|
default: unknown;
|
|
@@ -11,12 +11,15 @@ import type { LucidOptions } from './attachment.js';
|
|
|
11
11
|
export type Converter = {
|
|
12
12
|
options?: ConverterOptions;
|
|
13
13
|
binPaths?: BinPaths;
|
|
14
|
-
handle
|
|
14
|
+
handle: (attributes: ConverterAttributes) => Promise<Input | undefined>;
|
|
15
15
|
};
|
|
16
16
|
export type ConverterInitializeAttributes = {
|
|
17
17
|
record: Record;
|
|
18
18
|
attributeName: string;
|
|
19
19
|
options: LucidOptions;
|
|
20
|
+
filters?: {
|
|
21
|
+
variants?: string[];
|
|
22
|
+
};
|
|
20
23
|
};
|
|
21
24
|
export type ConverterAttributes = {
|
|
22
25
|
input: Input;
|
|
@@ -31,40 +34,40 @@ type jpeg = {
|
|
|
31
34
|
format: 'jpeg';
|
|
32
35
|
options: {
|
|
33
36
|
quality?: number;
|
|
34
|
-
progressive?:
|
|
37
|
+
progressive?: boolean;
|
|
35
38
|
chromaSubsampling?: string;
|
|
36
|
-
optimiseCoding?:
|
|
37
|
-
optimizeCoding?:
|
|
38
|
-
mozjpeg?:
|
|
39
|
-
trellisQuantisation?:
|
|
40
|
-
overshootDeringing?:
|
|
41
|
-
optimiseScans?:
|
|
42
|
-
optimizeScans?:
|
|
39
|
+
optimiseCoding?: boolean;
|
|
40
|
+
optimizeCoding?: boolean;
|
|
41
|
+
mozjpeg?: boolean;
|
|
42
|
+
trellisQuantisation?: boolean;
|
|
43
|
+
overshootDeringing?: boolean;
|
|
44
|
+
optimiseScans?: boolean;
|
|
45
|
+
optimizeScans?: boolean;
|
|
43
46
|
quantisationTable?: number;
|
|
44
47
|
quantizationTable?: number;
|
|
45
|
-
force?:
|
|
48
|
+
force?: boolean;
|
|
46
49
|
};
|
|
47
50
|
};
|
|
48
51
|
type png = {
|
|
49
52
|
format: 'png';
|
|
50
53
|
options: {
|
|
51
54
|
quality?: number;
|
|
52
|
-
progressive?:
|
|
55
|
+
progressive?: boolean;
|
|
53
56
|
compressionLevel?: number;
|
|
54
|
-
adaptiveFiltering?:
|
|
55
|
-
palette?:
|
|
57
|
+
adaptiveFiltering?: boolean;
|
|
58
|
+
palette?: boolean;
|
|
56
59
|
effort?: number;
|
|
57
60
|
colours?: number;
|
|
58
61
|
colors?: number;
|
|
59
62
|
dither?: number;
|
|
60
|
-
force?:
|
|
63
|
+
force?: boolean;
|
|
61
64
|
};
|
|
62
65
|
};
|
|
63
66
|
type gif = {
|
|
64
67
|
format: 'gif';
|
|
65
68
|
options: {
|
|
66
|
-
reuse?:
|
|
67
|
-
progressive?:
|
|
69
|
+
reuse?: boolean;
|
|
70
|
+
progressive?: boolean;
|
|
68
71
|
colours?: number;
|
|
69
72
|
colors?: number;
|
|
70
73
|
effort?: number;
|
|
@@ -73,7 +76,7 @@ type gif = {
|
|
|
73
76
|
interPaletteMaxError?: number;
|
|
74
77
|
loop?: number;
|
|
75
78
|
delay?: number | number[];
|
|
76
|
-
force?:
|
|
79
|
+
force?: boolean;
|
|
77
80
|
};
|
|
78
81
|
};
|
|
79
82
|
type webp = {
|
|
@@ -81,23 +84,23 @@ type webp = {
|
|
|
81
84
|
options: {
|
|
82
85
|
quality?: number;
|
|
83
86
|
alphaQuality?: number;
|
|
84
|
-
lossless?:
|
|
85
|
-
nearLossless?:
|
|
86
|
-
smartSubsample?:
|
|
87
|
+
lossless?: boolean;
|
|
88
|
+
nearLossless?: boolean;
|
|
89
|
+
smartSubsample?: boolean;
|
|
87
90
|
preset?: string;
|
|
88
91
|
effort?: number;
|
|
89
92
|
loop?: number;
|
|
90
93
|
delay?: number | number[];
|
|
91
|
-
minSize?:
|
|
92
|
-
mixed?:
|
|
93
|
-
force?:
|
|
94
|
+
minSize?: boolean;
|
|
95
|
+
mixed?: boolean;
|
|
96
|
+
force?: boolean;
|
|
94
97
|
};
|
|
95
98
|
};
|
|
96
99
|
type avif = {
|
|
97
100
|
format: 'avif';
|
|
98
101
|
options: {
|
|
99
102
|
quality?: number;
|
|
100
|
-
lossless?:
|
|
103
|
+
lossless?: boolean;
|
|
101
104
|
effort?: number;
|
|
102
105
|
chromaSubsampling?: string;
|
|
103
106
|
bitdepth?: number;
|
|
@@ -108,7 +111,7 @@ type heif = {
|
|
|
108
111
|
options: {
|
|
109
112
|
compression?: string;
|
|
110
113
|
quality?: number;
|
|
111
|
-
lossless?:
|
|
114
|
+
lossless?: boolean;
|
|
112
115
|
effort?: number;
|
|
113
116
|
chromaSubsampling?: string;
|
|
114
117
|
bitdepth?: number;
|
|
@@ -127,9 +130,9 @@ export type ConverterOptions = {
|
|
|
127
130
|
alpha: number;
|
|
128
131
|
};
|
|
129
132
|
kernel?: string;
|
|
130
|
-
withoutEnlargement?:
|
|
131
|
-
withoutReduction?:
|
|
132
|
-
fastShrinkOnLoad?:
|
|
133
|
+
withoutEnlargement?: boolean;
|
|
134
|
+
withoutReduction?: boolean;
|
|
135
|
+
fastShrinkOnLoad?: boolean;
|
|
133
136
|
};
|
|
134
137
|
format?: 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp' | 'avif' | 'heif' | 'tiff' | 'raw' | jpeg | png | gif | webp | avif | heif;
|
|
135
138
|
blurhash?: boolean | BlurhashOptions;
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { LucidRow } from '@adonisjs/lucid/types/model';
|
|
8
8
|
import type { Attachment } from './attachment.js';
|
|
9
|
-
export type
|
|
9
|
+
export type AttributeOfRowWithAttachment = {
|
|
10
10
|
attached: Attachment[];
|
|
11
11
|
detached: Attachment[];
|
|
12
12
|
dirtied: string[];
|
|
13
13
|
};
|
|
14
|
-
export type
|
|
15
|
-
$attachments:
|
|
14
|
+
export type RowWithAttachment = LucidRow & {
|
|
15
|
+
$attachments: AttributeOfRowWithAttachment;
|
|
16
16
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Attachment } from './attachment.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RowWithAttachment } from './mixin.js';
|
|
3
3
|
export interface Record {
|
|
4
|
-
|
|
4
|
+
row: RowWithAttachment;
|
|
5
5
|
commit(): Promise<void>;
|
|
6
6
|
rollback(): Promise<void>;
|
|
7
7
|
persist(): Promise<void>;
|
|
@@ -13,5 +13,6 @@ export interface Record {
|
|
|
13
13
|
getAttachments(options: {
|
|
14
14
|
attributeName: string;
|
|
15
15
|
requiredOriginal?: boolean;
|
|
16
|
+
requiredDirty?: boolean;
|
|
16
17
|
}): Attachment[];
|
|
17
18
|
}
|
|
@@ -13,3 +13,4 @@ export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<
|
|
|
13
13
|
export declare function downloadToTempFile(input: URL): Promise<string>;
|
|
14
14
|
export declare function isBase64(str: string): boolean;
|
|
15
15
|
export declare function imageToBlurhash(input: Input, options?: BlurhashOptions): Promise<string>;
|
|
16
|
+
export declare function extractPathParameters(path: string): string[];
|
|
@@ -110,3 +110,12 @@ export function imageToBlurhash(input, options) {
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
+
export function extractPathParameters(path) {
|
|
114
|
+
const paramRegex = /:(\w+)/g;
|
|
115
|
+
const parameters = [];
|
|
116
|
+
let match;
|
|
117
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
118
|
+
parameters.push(match[1]);
|
|
119
|
+
}
|
|
120
|
+
return parameters;
|
|
121
|
+
}
|