@jrmc/adonis-attachment 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,8 @@ import fs from 'node:fs/promises';
8
8
  import ExifReader from 'exifreader';
9
9
  import logger from '@adonisjs/core/services/logger';
10
10
  import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type';
11
- import { cleanObject } from '../utils/helpers.js';
11
+ import { cleanObject, use } from '../utils/helpers.js';
12
+ import { attachmentManager } from '../../index.js';
12
13
  export const exif = async (input) => {
13
14
  let fileType;
14
15
  let buffer;
@@ -104,10 +105,15 @@ async function imageExif(buffer) {
104
105
  }
105
106
  async function videoExif(input) {
106
107
  return new Promise(async (resolve) => {
107
- const module = 'fluent-ffmpeg';
108
- const result = await import(module);
109
- const ffmpeg = result.default;
110
- ffmpeg(input).ffprobe(0, (err, data) => {
108
+ const ffmpeg = await use('fluent-ffmpeg');
109
+ const ff = ffmpeg(input);
110
+ const config = attachmentManager.getConfig();
111
+ if (config.bin) {
112
+ if (config.bin.ffprobePath) {
113
+ ff.setFfprobePath(config.bin.ffprobePath);
114
+ }
115
+ }
116
+ ff.ffprobe(0, (err, data) => {
111
117
  if (err) {
112
118
  logger.error({ err });
113
119
  }
@@ -14,6 +14,7 @@ import Converter from './converters/converter.js';
14
14
  export declare class AttachmentManager {
15
15
  #private;
16
16
  constructor(config: ResolvedAttachmentConfig, logger: LoggerService, drive: DriveService);
17
+ getConfig(): ResolvedAttachmentConfig;
17
18
  createFromDbResponse(response: any): Attachment | null;
18
19
  createFromFile(file: MultipartFile): Promise<Attachment>;
19
20
  createFromBuffer(buffer: Buffer, name?: string): Promise<Attachment>;
@@ -7,6 +7,7 @@
7
7
  import { Exception } from '@poppinss/utils';
8
8
  import { Attachment } from './attachments/attachment.js';
9
9
  import { createAttachmentAttributes } from './utils/helpers.js';
10
+ import { exif } from './adapters/exif.js';
10
11
  const REQUIRED_ATTRIBUTES = ['name', 'size', 'extname', 'mimeType'];
11
12
  export class AttachmentManager {
12
13
  #logger;
@@ -17,6 +18,9 @@ export class AttachmentManager {
17
18
  this.#drive = drive;
18
19
  this.#config = config;
19
20
  }
21
+ getConfig() {
22
+ return this.#config;
23
+ }
20
24
  createFromDbResponse(response) {
21
25
  if (response === null) {
22
26
  return null;
@@ -55,8 +59,13 @@ export class AttachmentManager {
55
59
  }
56
60
  }
57
61
  async save(attachment) {
58
- await attachment.beforeSave();
59
62
  const destinationPath = attachment.path;
63
+ if (attachment.options?.meta) {
64
+ attachment.meta = await exif(attachment.input);
65
+ }
66
+ else {
67
+ attachment.meta = undefined;
68
+ }
60
69
  try {
61
70
  if (Buffer.isBuffer(attachment.input)) {
62
71
  await attachment.getDisk().put(destinationPath, attachment.input);
@@ -5,7 +5,7 @@
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
7
  import type { DriveService, SignedURLOptions } from '@adonisjs/drive/types';
8
- import type { AttachmentAttributes, Attachment as AttachmentInterface } from '../types/attachment.js';
8
+ import type { AttachmentAttributes, Attachment as AttachmentInterface, LucidOptions } from '../types/attachment.js';
9
9
  import type { Input } from '../types/input.js';
10
10
  import { AttachmentBase } from './attachment_base.js';
11
11
  import { Variant } from './variant_attachment.js';
@@ -17,7 +17,7 @@ export declare class Attachment extends AttachmentBase implements AttachmentInte
17
17
  getVariant(variantName: string): Variant | undefined;
18
18
  getUrl(variantName?: string): Promise<string>;
19
19
  getSignedUrl(variantNameOrOptions?: string | SignedURLOptions, signedUrlOptions?: SignedURLOptions): Promise<string>;
20
- beforeSave(): Promise<void>;
20
+ setOptions(options: LucidOptions): this;
21
21
  toObject(): AttachmentAttributes;
22
22
  toJSON(): Promise<Object>;
23
23
  }
@@ -67,14 +67,19 @@ export class Attachment extends AttachmentBase {
67
67
  }
68
68
  return super.getSignedUrl(options);
69
69
  }
70
- async beforeSave() {
71
- this.folder = this.options.folder;
72
- const outputPath = path.join(this.folder, this.name);
73
- if (!this.meta) {
74
- const data = await createAttachmentAttributes(this.input);
75
- this.meta = data.meta;
70
+ setOptions(options) {
71
+ this.options = {
72
+ ...this.options,
73
+ ...options,
74
+ };
75
+ if (!this.path) {
76
+ if (!this.options.rename) {
77
+ this.name = this.originalName;
78
+ }
79
+ this.folder = this.options.folder;
80
+ this.path = path.join(this.folder, this.name);
76
81
  }
77
- this.path = outputPath;
82
+ return this;
78
83
  }
79
84
  toObject() {
80
85
  const variants = this.variants?.map((v) => v.toObject());
@@ -22,8 +22,7 @@ export declare class AttachmentBase implements AttachmentBaseInterface {
22
22
  getDisk(): import("flydrive").Disk;
23
23
  getUrl(): Promise<string>;
24
24
  getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
25
- setOptions(options?: LucidOptions): this;
26
- beforeSave(): Promise<void>;
25
+ setOptions(options: LucidOptions): this;
27
26
  toObject(): AttachmentBaseAttributes;
28
27
  toJSON(): Promise<Object>;
29
28
  }
@@ -50,7 +50,6 @@ export class AttachmentBase {
50
50
  };
51
51
  return this;
52
52
  }
53
- async beforeSave() { }
54
53
  toObject() {
55
54
  return {
56
55
  name: this.name,
@@ -11,7 +11,6 @@ export class Variant extends AttachmentBase {
11
11
  folder;
12
12
  constructor(drive, attributes, input) {
13
13
  super(drive, attributes, input);
14
- this.meta = attributes.meta;
15
14
  this.key = attributes.key;
16
15
  this.folder = attributes.folder;
17
16
  this.path = path.join(this.folder, this.name);
@@ -26,9 +26,9 @@ export class ConverterManager {
26
26
  const attachment = record.$attributes[attribute];
27
27
  const data = {};
28
28
  const variant = await attachment.createVariant(this.#key, output);
29
+ await attachmentManager.save(variant);
29
30
  data[attribute] = JSON.stringify(attachment.toObject());
30
31
  const trx = await db.transaction();
31
- trx.after('commit', () => attachmentManager.save(variant));
32
32
  trx.after('rollback', () => attachmentManager.delete(variant));
33
33
  try {
34
34
  await trx.query().from(Model.table).where('id', id).update(data);
@@ -19,10 +19,10 @@ export default class AutodetectConverter extends Converter {
19
19
  fileType = await fileTypeFromFile(input);
20
20
  }
21
21
  if (fileType?.mime.includes('image')) {
22
- converter = new ImageConverter();
22
+ converter = new ImageConverter(options, this.binPaths);
23
23
  }
24
24
  else if (fileType?.mime.includes('video')) {
25
- converter = new VideoThumnailConverter();
25
+ converter = new VideoThumnailConverter(options, this.binPaths);
26
26
  }
27
27
  if (converter) {
28
28
  return await converter.handle({
@@ -4,8 +4,10 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { BinPaths } from '../types/config.js';
7
8
  import type { Converter as ConverterInterface, ConverterOptions } from '../types/converter.js';
8
9
  export default class Converter implements ConverterInterface {
9
10
  options?: ConverterOptions;
10
- constructor(options?: ConverterOptions);
11
+ binPaths?: BinPaths;
12
+ constructor(options?: ConverterOptions, binPaths?: BinPaths);
11
13
  }
@@ -6,7 +6,9 @@
6
6
  */
7
7
  export default class Converter {
8
8
  options;
9
- constructor(options) {
9
+ binPaths;
10
+ constructor(options, binPaths) {
10
11
  this.options = options;
12
+ this.binPaths = binPaths;
11
13
  }
12
14
  }
@@ -4,19 +4,11 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
- import logger from '@adonisjs/core/services/logger';
8
7
  import Converter from './converter.js';
8
+ import { use } from '../utils/helpers.js';
9
9
  export default class ImageConverter extends Converter {
10
10
  async handle({ input, options }) {
11
- let sharp;
12
- try {
13
- const module = 'sharp';
14
- const result = await import(module);
15
- sharp = result.default;
16
- }
17
- catch (error) {
18
- logger.error({ err: error }, 'Dependence missing, please install sharp');
19
- }
11
+ const sharp = await use('sharp');
20
12
  if (sharp) {
21
13
  const resize = options?.resize || {};
22
14
  let format = options?.format || 'webp';
@@ -6,21 +6,13 @@
6
6
  */
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
- import logger from '@adonisjs/core/services/logger';
10
9
  import { cuid } from '@adonisjs/core/helpers';
11
10
  import Converter from './converter.js';
12
11
  import ImageConverter from './image_converter.js';
12
+ import { use } from '../utils/helpers.js';
13
13
  export default class VideoThumbnailConvert extends Converter {
14
14
  async handle({ input, options }) {
15
- let ffmpeg;
16
- try {
17
- const module = 'fluent-ffmpeg';
18
- const result = await import(module);
19
- ffmpeg = result.default;
20
- }
21
- catch (error) {
22
- logger.error({ err: error }, 'Dependence missing, please install fluent-ffmpeg');
23
- }
15
+ const ffmpeg = await use('fluent-ffmpeg');
24
16
  if (ffmpeg) {
25
17
  const filePath = await this.videoToImage(ffmpeg, input);
26
18
  if (options && filePath) {
@@ -39,8 +31,13 @@ export default class VideoThumbnailConvert extends Converter {
39
31
  return new Promise((resolve) => {
40
32
  const folder = os.tmpdir();
41
33
  const filename = `${cuid()}.png`;
42
- ffmpeg(input)
43
- .screenshots({
34
+ const ff = ffmpeg(input);
35
+ if (this.binPaths) {
36
+ if (this.binPaths.ffmpegPath) {
37
+ ff.setFfmpegPath(this.binPaths.ffmpegPath);
38
+ }
39
+ }
40
+ ff.screenshots({
44
41
  count: 1,
45
42
  filename,
46
43
  folder,
@@ -12,10 +12,21 @@ export const attachment = (options) => {
12
12
  if (!target[optionsSym]) {
13
13
  target[optionsSym] = {};
14
14
  }
15
+ const defaultConfig = attachmentManager.getConfig();
16
+ const defaultOptions = {
17
+ meta: defaultConfig.meta !== undefined ? defaultConfig.meta : defaultOptionsDecorator.meta,
18
+ rename: defaultConfig.rename !== undefined ? defaultConfig.rename : defaultOptionsDecorator.rename,
19
+ };
20
+ if (!options || options?.meta === undefined) {
21
+ options.meta = defaultOptions.meta;
22
+ }
23
+ if (!options || options?.rename === undefined) {
24
+ options.rename = defaultOptions.rename;
25
+ }
15
26
  target[optionsSym][attributeName] = options;
16
27
  const Model = target.constructor;
17
28
  Model.boot();
18
- const { disk, folder, variants, ...columnOptions } = {
29
+ const { disk, folder, variants, meta, rename, ...columnOptions } = {
19
30
  ...defaultOptionsDecorator,
20
31
  ...options,
21
32
  };
@@ -23,7 +34,7 @@ export const attachment = (options) => {
23
34
  consume: (value) => {
24
35
  if (value) {
25
36
  const attachment = attachmentManager.createFromDbResponse(value);
26
- attachment?.setOptions({ disk, folder, variants });
37
+ attachment?.setOptions({ disk, folder, variants, meta, rename });
27
38
  return attachment;
28
39
  }
29
40
  else {
@@ -11,15 +11,17 @@ export function defineConfig(config) {
11
11
  let convertersMap = [];
12
12
  if (config.converters) {
13
13
  convertersMap = await Promise.all(config.converters.map(async (c) => {
14
+ const binConfig = config.bin;
14
15
  const { default: value } = await c.converter();
15
16
  const Converter = value;
16
17
  return {
17
18
  key: c.key,
18
- converter: new Converter(c.options),
19
+ converter: new Converter(c.options, binConfig),
19
20
  };
20
21
  }));
21
22
  }
22
23
  return {
24
+ ...config,
23
25
  converters: convertersMap,
24
26
  };
25
27
  });
@@ -22,8 +22,7 @@ export type AttachmentBase = {
22
22
  getDisk(): Disk;
23
23
  getUrl(): Promise<string>;
24
24
  getSignedUrl(signedUrlOptions?: SignedURLOptions): Promise<string>;
25
- setOptions(options?: LucidOptions): AttachmentBase;
26
- beforeSave(): Promise<void>;
25
+ setOptions(options: LucidOptions): AttachmentBase;
27
26
  toObject(): AttachmentBaseAttributes;
28
27
  toJSON(): Promise<Object>;
29
28
  };
@@ -45,6 +44,8 @@ export type LucidOptions = {
45
44
  disk?: string;
46
45
  folder?: string;
47
46
  variants?: string[];
47
+ rename?: boolean;
48
+ meta?: boolean;
48
49
  };
49
50
  export type AttachmentBaseAttributes = {
50
51
  name?: string;
@@ -13,7 +13,14 @@ type ConverterConfig = {
13
13
  converter: () => Promise<ImportConverter>;
14
14
  options?: ConverterOptions;
15
15
  };
16
+ export type BinPaths = {
17
+ ffmpegPath?: string;
18
+ ffprobePath?: string;
19
+ };
16
20
  export type AttachmentConfig = {
21
+ bin?: BinPaths;
22
+ meta?: boolean;
23
+ rename?: boolean;
17
24
  converters?: ConverterConfig[];
18
25
  };
19
26
  export type ResolvedConverter = {
@@ -21,6 +28,9 @@ export type ResolvedConverter = {
21
28
  converter: Converter;
22
29
  };
23
30
  export type ResolvedAttachmentConfig = {
31
+ bin?: BinPaths;
32
+ meta?: boolean;
33
+ rename?: boolean;
24
34
  converters?: ResolvedConverter[];
25
35
  };
26
36
  export {};
@@ -4,10 +4,12 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { BinPaths } from './config.js';
7
8
  import type { Input } from './input.js';
8
9
  import type { ModelWithAttachment } from './mixin.js';
9
10
  export type Converter = {
10
11
  options?: ConverterOptions;
12
+ binPaths?: BinPaths;
11
13
  handle?: (attributes: ConverterAttributes) => Promise<Input | undefined>;
12
14
  };
13
15
  export type ConverterInitializeAttributes = {
@@ -8,6 +8,8 @@ export declare const defaultOptionsDecorator: {
8
8
  disk: undefined;
9
9
  folder: string;
10
10
  variants: never[];
11
+ rename: boolean;
12
+ meta: boolean;
11
13
  };
12
14
  export declare const defaultStateAttributeMixin: {
13
15
  attached: never[];
@@ -8,6 +8,8 @@ export const defaultOptionsDecorator = {
8
8
  disk: undefined,
9
9
  folder: 'uploads',
10
10
  variants: [],
11
+ rename: true,
12
+ meta: true
11
13
  };
12
14
  export const defaultStateAttributeMixin = {
13
15
  attached: [],
@@ -14,7 +14,7 @@ export declare function createAttachmentAttributes(input: Input, name?: string):
14
14
  extname: import("file-type").FileExtension;
15
15
  mimeType: import("file-type").MimeType;
16
16
  size: number;
17
- meta: import("../types/input.js").Exif | undefined;
18
17
  }>;
19
18
  export declare function cleanObject(obj: any): any;
20
19
  export declare function clone(object: Object): any;
20
+ export declare function use(module: string): Promise<any>;
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import { cuid } from '@adonisjs/core/helpers';
8
8
  import string from '@adonisjs/core/helpers/string';
9
+ import logger from '@adonisjs/core/services/logger';
9
10
  import { fileTypeFromBuffer, fileTypeFromFile } from 'file-type';
10
11
  import { Attachment } from '../attachments/attachment.js';
11
12
  import { optionsSym } from './symbols.js';
12
- import { exif } from '../adapters/exif.js';
13
13
  export function getAttachmentAttributeNames(modelInstance) {
14
14
  return Object.keys(modelInstance.$attributes).filter((attr) => modelInstance.$attributes[attr] instanceof Attachment);
15
15
  }
@@ -24,7 +24,6 @@ export async function createAttachmentAttributes(input, name) {
24
24
  else {
25
25
  fileType = await fileTypeFromFile(input);
26
26
  }
27
- const meta = await exif(input);
28
27
  if (name) {
29
28
  name = string.slug(name);
30
29
  }
@@ -36,7 +35,6 @@ export async function createAttachmentAttributes(input, name) {
36
35
  extname: fileType.ext,
37
36
  mimeType: fileType.mime,
38
37
  size: input.length,
39
- meta: meta,
40
38
  };
41
39
  }
42
40
  export function cleanObject(obj) {
@@ -60,3 +58,12 @@ export function cleanObject(obj) {
60
58
  export function clone(object) {
61
59
  return JSON.parse(JSON.stringify(object));
62
60
  }
61
+ export async function use(module) {
62
+ try {
63
+ const result = await import(module);
64
+ return result.default;
65
+ }
66
+ catch (error) {
67
+ logger.error({ err: error }, `Dependence missing, please install ${module}`);
68
+ }
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrmc/adonis-attachment",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Turn any field on your Lucid model to an attachment data type",
6
6
  "engines": {
@@ -77,7 +77,7 @@
77
77
  "peerDependencies": {
78
78
  "@adonisjs/core": "^6.12.1",
79
79
  "@adonisjs/drive": "^3.2.0",
80
- "@adonisjs/lucid": "^20.6.0"
80
+ "@adonisjs/lucid": "^20.6.0 || ^21.0.0"
81
81
  },
82
82
  "prettier": "@adonisjs/prettier-config",
83
83
  "publishConfig": {
@@ -85,6 +85,6 @@
85
85
  "tag": "latest"
86
86
  },
87
87
  "volta": {
88
- "node": "20.16.0"
88
+ "node": "20.17.0"
89
89
  }
90
90
  }