@jrmc/adonis-attachment 3.0.0 → 3.2.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/README.md CHANGED
@@ -17,6 +17,10 @@ Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-
17
17
  ## Roadmap
18
18
 
19
19
  - [x] attachment file by file system
20
+ - [x] attachment file by buffer
21
+ - [x] attachment file by path
22
+ - [x] attachment file by url
23
+ - [x] attachment file by stream
20
24
  - [x] save meta data
21
25
  - [x] variantes
22
26
  - [x] images
@@ -5,7 +5,7 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { ApplicationService } from '@adonisjs/core/types';
8
- import { AttachmentService } from '../src/types/config.js';
8
+ import type { AttachmentService } from '../src/types/config.js';
9
9
  declare module '@adonisjs/core/types' {
10
10
  interface ContainerBindings {
11
11
  'jrmc.attachment': AttachmentService;
@@ -5,4 +5,10 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { Exif, Input } from '../types/input.js';
8
- export declare const exif: (input: Input) => Promise<Exif | undefined>;
8
+ import { ResolvedAttachmentConfig } from '../define_config.js';
9
+ import { Converter } from '../types/converter.js';
10
+ type KnownConverters = Record<string, Converter>;
11
+ declare const _default: {
12
+ exif(input: Input, config: ResolvedAttachmentConfig<KnownConverters>): Promise<Exif | undefined>;
13
+ };
14
+ export default _default;
@@ -9,8 +9,12 @@ import ExifReader from 'exifreader';
9
9
  import logger from '@adonisjs/core/services/logger';
10
10
  import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type';
11
11
  import { bufferToTempFile, cleanObject, use } from '../utils/helpers.js';
12
- import { attachmentManager } from '../../index.js';
13
- export const exif = async (input) => {
12
+ export default {
13
+ async exif(input, config) {
14
+ return exif(input, config);
15
+ },
16
+ };
17
+ const exif = async (input, config) => {
14
18
  let fileType;
15
19
  let buffer;
16
20
  if (Buffer.isBuffer(input)) {
@@ -26,7 +30,7 @@ export const exif = async (input) => {
26
30
  }
27
31
  }
28
32
  if (fileType?.mime.includes('video')) {
29
- return videoExif(input);
33
+ return videoExif(input, config);
30
34
  }
31
35
  if (buffer && fileType?.mime.includes('image')) {
32
36
  return imageExif(buffer);
@@ -103,7 +107,7 @@ async function imageExif(buffer) {
103
107
  }
104
108
  return cleanObject(data);
105
109
  }
106
- async function videoExif(input) {
110
+ async function videoExif(input, config) {
107
111
  return new Promise(async (resolve) => {
108
112
  const ffmpeg = await use('fluent-ffmpeg');
109
113
  let file = input;
@@ -111,7 +115,6 @@ async function videoExif(input) {
111
115
  file = await bufferToTempFile(input);
112
116
  }
113
117
  const ff = ffmpeg(file);
114
- const config = attachmentManager.getConfig();
115
118
  if (config.bin) {
116
119
  if (config.bin.ffprobePath) {
117
120
  ff.setFfprobePath(config.bin.ffprobePath);
@@ -0,0 +1,3 @@
1
+ import { Meta } from '../types/input.js';
2
+ export declare function metaFormBuffer(input: Buffer): Promise<Meta>;
3
+ export declare function metaFormFile(input: string, filename: string): Promise<Meta>;
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type';
4
+ import mime from 'mime-types';
5
+ function getFileExtension(filename) {
6
+ if (!filename) {
7
+ return '';
8
+ }
9
+ const ext = path.extname(filename).slice(1);
10
+ return ext && /^[a-zA-Z0-9]+$/.test(ext) ? ext : '';
11
+ }
12
+ function metaByFileName(filename) {
13
+ return {
14
+ ext: getFileExtension(filename),
15
+ mime: mime.lookup(filename) || '',
16
+ };
17
+ }
18
+ export async function metaFormBuffer(input) {
19
+ const fileType = await fileTypeFromBuffer(input);
20
+ return {
21
+ extname: fileType?.ext || '',
22
+ mimeType: fileType?.mime || '',
23
+ size: input.length,
24
+ };
25
+ }
26
+ export async function metaFormFile(input, filename) {
27
+ let fileType;
28
+ let size = 0;
29
+ fileType = metaByFileName(filename);
30
+ if (fileType.ext === '' || fileType.mime === '') {
31
+ fileType = await fileTypeFromFile(input);
32
+ }
33
+ const stats = await fs.stat(input);
34
+ size = stats.size;
35
+ return {
36
+ extname: fileType?.ext || '',
37
+ mimeType: fileType?.mime || '',
38
+ size,
39
+ };
40
+ }
@@ -16,9 +16,12 @@ export declare class AttachmentManager<KnownConverters extends Record<string, Co
16
16
  constructor(config: ResolvedAttachmentConfig<KnownConverters>, drive: DriveService);
17
17
  getConfig(): ResolvedAttachmentConfig<KnownConverters>;
18
18
  createFromDbResponse(response: any): AttachmentType | null;
19
- createFromFile(file: MultipartFile): Promise<AttachmentType>;
20
- createFromBuffer(buffer: Buffer, name?: string): Promise<AttachmentType>;
21
- createFromBase64(data: string, name?: string): Promise<AttachmentType>;
19
+ createFromFile(input: MultipartFile): Promise<AttachmentType>;
20
+ createFromPath(input: string, name?: string): Promise<AttachmentType>;
21
+ createFromBuffer(input: Buffer, name?: string): Promise<AttachmentType>;
22
+ createFromBase64(input: string, name?: string): Promise<AttachmentType>;
23
+ createFromUrl(input: URL, name?: string): Promise<AttachmentType>;
24
+ createFromStream(stream: NodeJS.ReadableStream, name?: string): Promise<AttachmentType>;
22
25
  getConverter(key: string): Promise<void | Converter>;
23
26
  computeUrl(attachment: AttachmentType | AttachmentBase, signedUrlOptions?: SignedURLOptions): Promise<void>;
24
27
  preComputeUrl(attachment: AttachmentType): Promise<void>;
@@ -4,11 +4,14 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import path from 'node:path';
7
8
  import { DeferQueue } from '@poppinss/defer';
8
9
  import * as errors from './errors.js';
9
10
  import { Attachment } from './attachments/attachment.js';
10
- import { createAttachmentAttributes, isBase64 } from './utils/helpers.js';
11
- import { exif } from './adapters/exif.js';
11
+ import { downloadToTempFile, isBase64, streamToTempFile } from './utils/helpers.js';
12
+ import ExifAdapter from './adapters/exif.js';
13
+ import { metaFormBuffer, metaFormFile } from './adapters/meta.js';
14
+ import { cuid } from '@adonisjs/core/helpers';
12
15
  const REQUIRED_ATTRIBUTES = ['name', 'size', 'extname', 'mimeType'];
13
16
  export class AttachmentManager {
14
17
  queue;
@@ -36,34 +39,60 @@ export class AttachmentManager {
36
39
  const attachment = new Attachment(this.#drive, attributes);
37
40
  return this.#configureAttachment(attachment);
38
41
  }
39
- async createFromFile(file) {
42
+ async createFromFile(input) {
40
43
  const attributes = {
41
- originalName: file.clientName,
42
- extname: file.extname,
43
- mimeType: `${file.type}/${file.subtype}`,
44
- size: file.size,
44
+ originalName: input.clientName,
45
+ extname: input.extname,
46
+ mimeType: `${input.type}/${input.subtype}`,
47
+ size: input.size,
45
48
  };
46
- if (!file.tmpPath) {
49
+ if (!input.tmpPath) {
47
50
  throw new errors.ENOENT();
48
51
  }
49
- const attachment = new Attachment(this.#drive, attributes, file.tmpPath);
52
+ const attachment = new Attachment(this.#drive, attributes, input.tmpPath);
50
53
  return this.#configureAttachment(attachment);
51
54
  }
52
- async createFromBuffer(buffer, name) {
53
- if (!Buffer.isBuffer(buffer)) {
55
+ async createFromPath(input, name) {
56
+ const meta = await metaFormFile(input, name || input);
57
+ if (meta.extname === '') {
58
+ meta.extname = 'tmp';
59
+ meta.mimeType = 'application/x-temp';
60
+ }
61
+ const attributes = {
62
+ ...meta,
63
+ originalName: name?.replace('tmp', meta.extname) || path.basename(input),
64
+ };
65
+ const attachment = new Attachment(this.#drive, attributes, input);
66
+ return this.#configureAttachment(attachment);
67
+ }
68
+ async createFromBuffer(input, name) {
69
+ if (!Buffer.isBuffer(input)) {
54
70
  throw new errors.E_ISNOT_BUFFER();
55
71
  }
56
- const attributes = await createAttachmentAttributes(buffer, name);
57
- const attachment = new Attachment(this.#drive, attributes, buffer);
72
+ const meta = await metaFormBuffer(input);
73
+ const ext = meta.extname || 'tmp';
74
+ const attributes = {
75
+ ...meta,
76
+ originalName: name || `${cuid()}.${ext}`,
77
+ };
78
+ const attachment = new Attachment(this.#drive, attributes, input);
58
79
  return this.#configureAttachment(attachment);
59
80
  }
60
- async createFromBase64(data, name) {
61
- const base64Data = data.replace(/^data:([A-Za-z-+\/]+);base64,/, '');
81
+ async createFromBase64(input, name) {
82
+ const base64Data = input.replace(/^data:([A-Za-z-+\/]+);base64,/, '');
62
83
  if (!isBase64(base64Data)) {
63
84
  throw new errors.E_ISNOT_BASE64();
64
85
  }
65
86
  const buffer = Buffer.from(base64Data, 'base64');
66
- return await this.createFromBuffer(buffer, name);
87
+ return this.createFromBuffer(buffer, name);
88
+ }
89
+ async createFromUrl(input, name) {
90
+ const tmpPath = await downloadToTempFile(input);
91
+ return this.createFromPath(tmpPath, name || path.basename(input.pathname));
92
+ }
93
+ async createFromStream(stream, name) {
94
+ const tmpPath = await streamToTempFile(stream);
95
+ return this.createFromPath(tmpPath, name || path.basename(tmpPath));
67
96
  }
68
97
  async getConverter(key) {
69
98
  if (this.#config.converters) {
@@ -96,7 +125,7 @@ export class AttachmentManager {
96
125
  async save(attachment) {
97
126
  const destinationPath = attachment.path;
98
127
  if (attachment.options?.meta) {
99
- attachment.meta = await exif(attachment.input);
128
+ attachment.meta = await ExifAdapter.exif(attachment.input, this.#config);
100
129
  }
101
130
  else {
102
131
  attachment.meta = undefined;
@@ -7,7 +7,7 @@
7
7
  import path from 'node:path';
8
8
  import { AttachmentBase } from './attachment_base.js';
9
9
  import { Variant } from './variant_attachment.js';
10
- import { createAttachmentAttributes } from '../utils/helpers.js';
10
+ import { metaFormBuffer, metaFormFile } from '../adapters/meta.js';
11
11
  export class Attachment extends AttachmentBase {
12
12
  originalName;
13
13
  variants;
@@ -35,8 +35,15 @@ export class Attachment extends AttachmentBase {
35
35
  * Methods
36
36
  */
37
37
  async createVariant(key, input) {
38
+ let meta;
39
+ if (Buffer.isBuffer(input)) {
40
+ meta = await metaFormBuffer(input);
41
+ }
42
+ else {
43
+ meta = await metaFormFile(input, this.name);
44
+ }
38
45
  const attributes = {
39
- ...(await createAttachmentAttributes(input)),
46
+ ...meta,
40
47
  key,
41
48
  folder: path.join(this.options.folder, 'variants', this.name),
42
49
  };
@@ -7,6 +7,9 @@
7
7
  import attachmentManager from '../../services/main.js';
8
8
  import { optionsSym } from '../utils/symbols.js';
9
9
  import { defaultOptionsDecorator } from '../utils/default_values.js';
10
+ import { afterFindHook, afterFetchHook, beforeSaveHook, afterSaveHook, beforeDeleteHook, } from '../utils/hooks.js';
11
+ import { clone } from '../utils/helpers.js';
12
+ import { defaultStateAttributeMixin } from '../utils/default_values.js';
10
13
  export const attachment = (options) => {
11
14
  return function (target, attributeName) {
12
15
  if (!target[optionsSym]) {
@@ -15,6 +18,28 @@ export const attachment = (options) => {
15
18
  target[optionsSym][attributeName] = options;
16
19
  const Model = target.constructor;
17
20
  Model.boot();
21
+ Model.$attachments = clone(defaultStateAttributeMixin);
22
+ /**
23
+ * Registering all hooks only once
24
+ */
25
+ if (!Model.$hooks.has('find', afterFindHook)) {
26
+ Model.after('find', afterFindHook);
27
+ }
28
+ if (!Model.$hooks.has('fetch', afterFetchHook)) {
29
+ Model.after('fetch', afterFetchHook);
30
+ }
31
+ if (!Model.$hooks.has('paginate', afterFetchHook)) {
32
+ Model.after('paginate', afterFetchHook);
33
+ }
34
+ if (!Model.$hooks.has('save', beforeSaveHook)) {
35
+ Model.before('save', beforeSaveHook);
36
+ }
37
+ if (!Model.$hooks.has('save', afterSaveHook)) {
38
+ Model.after('save', afterSaveHook);
39
+ }
40
+ if (!Model.$hooks.has('delete', beforeDeleteHook)) {
41
+ Model.before('delete', beforeDeleteHook);
42
+ }
18
43
  const { disk, folder, variants, meta, rename, ...columnOptions } = {
19
44
  ...defaultOptionsDecorator,
20
45
  ...options,
@@ -28,6 +28,10 @@ export declare const E_CANNOT_SET_VISIBILITY: new (args: [key: string], options?
28
28
  * Unable to generate URL for a file
29
29
  */
30
30
  export declare const E_CANNOT_GENERATE_URL: new (args: [key: string], options?: ErrorOptions) => import("@adonisjs/core/exceptions").Exception;
31
+ /**
32
+ * Unable to generate temp file
33
+ */
34
+ export declare const E_CANNOT_GENERATE_TEMP_FILE: new (args: [key: string], options?: ErrorOptions) => import("@adonisjs/core/exceptions").Exception;
31
35
  /**
32
36
  * The file key has unallowed set of characters
33
37
  */
@@ -26,6 +26,10 @@ export const E_CANNOT_SET_VISIBILITY = errors.E_CANNOT_SET_VISIBILITY;
26
26
  * Unable to generate URL for a file
27
27
  */
28
28
  export const E_CANNOT_GENERATE_URL = errors.E_CANNOT_GENERATE_URL;
29
+ /**
30
+ * Unable to generate temp file
31
+ */
32
+ export const E_CANNOT_GENERATE_TEMP_FILE = createError('Cannot generate temp file "%s"', 'E_CANNOT_GENERATE_TEMP_FILE');
29
33
  /**
30
34
  * The file key has unallowed set of characters
31
35
  */