├── .editorconfig ├── .github └── workflows │ ├── doc-deploy.yml │ ├── issue.yml │ ├── npm-publish.yml │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── bin └── test.ts ├── commands └── make │ └── converter.ts ├── configure.ts ├── docs ├── .vitepress │ └── config.ts ├── changelog.md ├── guide │ ├── advanced_usage │ │ ├── custom-converter.md │ │ ├── exceptions.md │ │ ├── pre-compile-on-demand.md │ │ ├── queue.md │ │ └── regenerate-variant.md │ ├── basic_usage │ │ ├── attachment-object.md │ │ ├── controller-setup.md │ │ ├── migration-setup.md │ │ ├── model-setup.md │ │ └── view-setup.md │ ├── converters │ │ ├── document-thumbnail.md │ │ ├── image.md │ │ ├── pdf-thumbnail.md │ │ └── video-thumbnail.md │ ├── essentials │ │ ├── configuration.md │ │ ├── installation.md │ │ └── introduction.md │ ├── partials │ │ ├── install-document.md │ │ ├── install-image.md │ │ ├── install-pdf.md │ │ ├── install-video.md │ │ └── table-converter.md │ ├── start-here.md │ └── use-cases │ │ └── picture.md ├── index.md ├── public │ ├── dark-convert.svg │ ├── dark-upload.svg │ ├── dark-view.svg │ ├── light-convert.svg │ ├── light-upload.svg │ └── light-view.svg ├── structure-data-json.md ├── v2 │ └── guide │ │ ├── advanced_usage │ │ ├── custom-converter.md │ │ ├── exceptions.md │ │ ├── pre-compile-on-demand.md │ │ └── queue.md │ │ ├── basic_usage │ │ ├── controller-setup.md │ │ ├── migration-setup.md │ │ ├── model-setup.md │ │ └── view-setup.md │ │ ├── converters │ │ ├── document-thumbnail.md │ │ ├── image.md │ │ ├── pdf-thumbnail.md │ │ └── video-thumbnail.md │ │ ├── essentials │ │ ├── configuration.md │ │ ├── installation.md │ │ └── introduction.md │ │ ├── partials │ │ ├── install-document.md │ │ ├── install-image.md │ │ ├── install-pdf.md │ │ ├── install-video.md │ │ └── table-converter.md │ │ ├── start-here.md │ │ └── use-cases │ │ └── picture.md └── v3 │ └── guide │ ├── advanced_usage │ ├── custom-converter.md │ ├── exceptions.md │ ├── pre-compile-on-demand.md │ └── queue.md │ ├── basic_usage │ ├── controller-setup.md │ ├── migration-setup.md │ ├── model-setup.md │ └── view-setup.md │ ├── converters │ ├── document-thumbnail.md │ ├── image.md │ ├── pdf-thumbnail.md │ └── video-thumbnail.md │ ├── essentials │ ├── configuration.md │ ├── installation.md │ └── introduction.md │ ├── partials │ ├── install-document.md │ ├── install-image.md │ ├── install-pdf.md │ ├── install-video.md │ └── table-converter.md │ ├── start-here.md │ └── use-cases │ └── picture.md ├── index.ts ├── package-lock.json ├── package.json ├── providers └── attachment_provider.ts ├── services ├── main.ts └── regenerate_service.ts ├── src ├── adapters │ ├── blurhash.ts │ ├── exif.ts │ └── meta.ts ├── attachment_manager.ts ├── attachments │ ├── attachment.ts │ ├── attachment_base.ts │ └── variant_attachment.ts ├── converter_manager.ts ├── converters │ ├── autodetect_converter.ts │ ├── converter.ts │ ├── document_thumbnail_converter.ts │ ├── image_converter.ts │ ├── pdf_thumbnail_converter.ts │ └── video_thumbnail_converter.ts ├── decorators │ └── attachment.ts ├── define_config.ts ├── errors.ts ├── services │ └── record_with_attachment.ts ├── types │ ├── attachment.ts │ ├── config.ts │ ├── converter.ts │ ├── index.ts │ ├── input.ts │ ├── mixin.ts │ ├── regenerate.ts │ └── service.ts └── utils │ ├── default_values.ts │ ├── helpers.ts │ ├── hooks.ts │ └── symbols.ts ├── stubs ├── config.stub ├── main.ts └── make │ └── converter │ └── main.stub ├── tests ├── attachment-create-by.spec.ts ├── attachment-manager.spec.ts ├── attachment-svg.spec.ts ├── attachment-variants-regenerate.spec.ts ├── attachment-variants.spec.ts ├── attachment.spec.ts ├── commands.spec.ts ├── fixtures │ ├── converters │ │ └── image_converter.ts │ ├── factories │ │ └── user.ts │ ├── images │ │ ├── adonis.svg │ │ ├── adonisjs.svg │ │ └── img.jpg │ └── migrations │ │ └── create_users_table.ts ├── helpers │ ├── app.ts │ └── index.ts └── options.spec.ts ├── tsconfig.json └── tsnode.esm.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = unset 13 | 14 | [**.min.js] 15 | indent_style = unset 16 | insert_final_newline = unset 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/workflows/doc-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Adonis Attachment site to Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | env: 9 | success: ✅ 10 | failure: 🔴 11 | cancelled: ❌ 12 | skipped: ⭕ 13 | 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | concurrency: 20 | group: pages 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | - name: Setup Node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | cache: npm 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v4 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Build with VitePress 41 | run: npm run docs:build 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs/.vitepress/dist 46 | 47 | # Deployment job 48 | deploy: 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | needs: build 53 | runs-on: ubuntu-latest 54 | name: Deploy 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | 60 | - name: Notification 61 | if: ${{ always() }} 62 | uses: appleboy/telegram-action@master 63 | with: 64 | token: ${{ secrets.BOT_TOKEN }} 65 | to: ${{ secrets.CHAT_ID }} 66 | message: | 67 | Repository: ${{ github.repository }} 68 | Doc publish : ${{ env[job.status] }} 69 | -------------------------------------------------------------------------------- /.github/workflows/issue.yml: -------------------------------------------------------------------------------- 1 | name: Notify on Issue or Comment 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | issue_comment: 7 | types: [created, edited] 8 | 9 | jobs: 10 | notify-discord: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Notification 15 | uses: appleboy/telegram-action@master 16 | with: 17 | token: ${{ secrets.BOT_TOKEN }} 18 | to: ${{ secrets.CHAT_ID }} 19 | message: | 20 | Repository: ${{ github.repository }} 21 | Event: ${{ github.event_name }} 22 | Action: ${{ github.event.action }} 23 | 24 | Issue Title: ${{ github.event.issue.title }} 25 | Issue URL: ${{ github.event.issue.html_url }} 26 | 27 | User: ${{ github.event.issue.user.login }} 28 | Comment: ${{ github.event.comment.body || 'No comment' }} 29 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | env: 9 | success: ✅ 10 | failure: 🔴 11 | cancelled: ❌ 12 | skipped: ⭕ 13 | node_version: 20 14 | true: ✅ 15 | false: 🔴 16 | 17 | jobs: 18 | test: 19 | name: Test 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - name: Update packages 23 | run: sudo apt-get update 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Setup node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ env.node_version }} 32 | registry-url: https://registry.npmjs.org/ 33 | cache: 'npm' 34 | 35 | - name: Install Dependencies 36 | run: npm ci 37 | 38 | - name: Run tests 39 | run: npm test 40 | 41 | - name: Notification 42 | if: ${{ always() }} 43 | uses: appleboy/telegram-action@master 44 | with: 45 | token: ${{ secrets.BOT_TOKEN }} 46 | to: ${{ secrets.CHAT_ID }} 47 | disable_web_page_preview: true 48 | message: | 49 | Test ${{ env[job.status] }} by ${{ github.actor }} for commit in ${{ github.ref_name }} branch. 50 | Comment: ${{ github.event.commits[0].message }} 51 | 52 | Repository: ${{ github.repository }} 53 | See changes: https://github.com/${{ github.repository }}/commit/${{ github.sha }} 54 | Github action: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 55 | 56 | release: 57 | needs: test 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v3 61 | 62 | - id: release 63 | uses: halvardssm/github-action-tag-release@1.0.0 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | path: './package.json' # optional, will use ./package.json by default 67 | 68 | - run: | 69 | echo 'Release created: ${{steps.release.outputs.release_created}}' # 'true' or 'false' 70 | echo 'Release exists: ${{steps.release.outputs.release_exists}}' # 'true' or 'false' 71 | echo 'Release tag: ${{steps.version.outputs.release_tag}}' # The tag from package.json 72 | 73 | - name: Notification 74 | if: ${{ always() }} 75 | uses: appleboy/telegram-action@master 76 | with: 77 | token: ${{ secrets.BOT_TOKEN }} 78 | to: ${{ secrets.CHAT_ID }} 79 | message: | 80 | Repository: ${{ github.repository }} 81 | Release : ${{ env[steps.release.outputs.release_created] }} 82 | Tag: ${{ steps.version.outputs.release_tag }} 83 | 84 | publish-npm: 85 | needs: release 86 | runs-on: ubuntu-latest 87 | steps: 88 | - uses: actions/checkout@v3 89 | 90 | - uses: actions/setup-node@v3 91 | with: 92 | node-version: 20 93 | registry-url: https://registry.npmjs.org/ 94 | 95 | - run: npm ci 96 | - run: npm publish 97 | env: 98 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 99 | 100 | - name: Notification 101 | if: ${{ always() }} 102 | uses: appleboy/telegram-action@master 103 | with: 104 | token: ${{ secrets.BOT_TOKEN }} 105 | to: ${{ secrets.CHAT_ID }} 106 | message: | 107 | Repository: ${{ github.repository }} 108 | npm publish : ${{ env[job.status] }} 109 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Notify on Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, closed] 6 | 7 | jobs: 8 | notify-telegram: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Send Telegram Notification 13 | uses: appleboy/telegram-action@master 14 | with: 15 | token: ${{ secrets.BOT_TOKEN }} 16 | to: ${{ secrets.CHAT_ID }} 17 | message: | 18 | Repository: ${{ github.repository }} 19 | Event Type: ${{ github.event_name }} 20 | Action: ${{ github.event.action }} 21 | User: ${{ github.event.sender.login }} 22 | 23 | Pull Request Title: ${{ github.event.pull_request.title }} 24 | Pull Request URL: ${{ github.event.pull_request.html_url }} 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Notify Discord on Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | notify-discord: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Send notification to Discord 14 | env: 15 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }} 16 | uses: Ilshidur/action-discord@0.3.2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | build 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | 134 | # vitepress 135 | docs/.vitepress/dist 136 | docs/.vitepress/cache 137 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jeremy Chaufourier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdonisJS attachment 2 | 3 | This package is currently development and will replace [attachment-advanced](https://github.com/batosai/attachment-advanced) for AdonisJS 6. 4 | 5 | ## Links 6 | 7 | [View documentation](https://adonis-attachment.jrmc.dev/) 8 | 9 | [ChangeLog](https://adonis-attachment.jrmc.dev/changelog.html) 10 | 11 | [Discord](https://discord.gg/89eMn2vB) 12 | 13 | Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit) 14 | 15 | ## Roadmap 16 | 17 | - [x] attachment file by file system 18 | - [x] attachment file by buffer 19 | - [x] attachment file by path 20 | - [x] attachment file by url 21 | - [x] attachment file by stream 22 | - [x] attachment files 23 | - [x] save meta data 24 | - [x] variantes 25 | - [x] images 26 | - [x] [blurhash](https://blurha.sh/) 27 | - [x] documents thumbnail 28 | - [x] videos thumbnail 29 | - [x] command regenerate 30 | - [x] command make:convert 31 | - [x] adonis-drive/flydrive 32 | - [x] jobs queue 33 | - [x] serialize 34 | 35 | 36 | ## Setup 37 | 38 | Install and configure the package: 39 | 40 | ```sh 41 | node ace add @jrmc/adonis-attachment 42 | ``` 43 | 44 | ## Sample 45 | 46 | Simple upload file 47 | 48 | ```ts 49 | // app/models/user.ts 50 | import { BaseModel } from '@adonisjs/lucid/orm' 51 | import { compose } from '@adonisjs/core/helpers' 52 | import { attachment, Attachmentable } from '@jrmc/adonis-attachment' 53 | import type { Attachment } from '@jrmc/adonis-attachment/types/attachment' 54 | 55 | class User extends compose(BaseModel, Attachmentable) { 56 | @attachment() 57 | declare avatar: Attachment 58 | } 59 | ``` 60 | 61 | --- 62 | 63 | ```ts 64 | // app/controllers/users_controller.ts 65 | import { attachmentManager } from '@jrmc/adonis-attachment' 66 | 67 | class UsersController { 68 | public store({ request }: HttpContext) { 69 | const avatar = request.file('avatar')! 70 | const user = new User() 71 | 72 | user.avatar = await attachmentManager.createFromFile(avatar) 73 | await user.save() 74 | } 75 | } 76 | ``` 77 | 78 | --- 79 | 80 | ```edge 81 | 82 | ``` 83 | 84 | Read [documentation](https://adonis-attachment.jrmc.dev/) for advanced usage(thumbnail video/pdf/doc, create from buffer/base64...) 85 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { expectTypeOf } from '@japa/expect-type' 3 | import { processCLIArgs, configure, run } from '@japa/runner' 4 | import { createApp, initializeDatabase, removeDatabase } from '../tests/helpers/app.js' 5 | import { fileSystem } from '@japa/file-system' 6 | import app from '@adonisjs/core/services/app' 7 | import { ApplicationService } from '@adonisjs/core/types' 8 | import { BASE_URL } from '../tests/helpers/index.js' 9 | 10 | let testApp: ApplicationService 11 | processCLIArgs(process.argv.slice(2)) 12 | configure({ 13 | files: ['tests/**/*.spec.ts'], 14 | plugins: [assert(), fileSystem({ basePath: BASE_URL }), expectTypeOf()], 15 | setup: [ 16 | async () => { 17 | testApp = await createApp() 18 | await initializeDatabase(testApp) 19 | }, 20 | ], 21 | teardown: [ 22 | async () => { 23 | await app.terminate() 24 | await testApp.terminate() 25 | await removeDatabase() 26 | }, 27 | ], 28 | }) 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Run tests 33 | |-------------------------------------------------------------------------- 34 | | 35 | | The following "run" method is required to execute all the tests. 36 | | 37 | */ 38 | run() 39 | -------------------------------------------------------------------------------- /commands/make/converter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jrmc/adonis-attachment 3 | * 4 | * @license MIT 5 | * @copyright Jeremy Chaufourier 6 | */ 7 | 8 | import { stubsRoot } from '../../stubs/main.js' 9 | import { args, BaseCommand } from '@adonisjs/core/ace' 10 | 11 | /** 12 | * The make controller command to create an HTTP controller 13 | */ 14 | export default class MakeConverter extends BaseCommand { 15 | static commandName = 'make:converter' 16 | static description = 'Create a new media converter class' 17 | 18 | @args.string({ description: 'The name of the converter' }) 19 | declare name: string 20 | 21 | protected stubPath: string = 'make/converter/main.stub' 22 | 23 | async run() { 24 | const codemods = await this.createCodemods() 25 | await codemods.makeUsingStub(stubsRoot, this.stubPath, { 26 | name: this.name, 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /configure.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jrmc/adonis-attachment 3 | * 4 | * @license MIT 5 | * @copyright Jeremy Chaufourier 6 | */ 7 | 8 | import type Configure from '@adonisjs/core/commands/configure' 9 | import { stubsRoot } from './stubs/main.js' 10 | 11 | export async function configure(command: Configure) { 12 | const codemods = await command.createCodemods() 13 | 14 | /** 15 | * Create default config file 16 | */ 17 | await codemods.makeUsingStub(stubsRoot, 'config.stub', {}) 18 | 19 | /** 20 | * Register provider 21 | */ 22 | await codemods.updateRcFile((rcFile) => { 23 | rcFile.addProvider('@jrmc/adonis-attachment/attachment_provider') 24 | rcFile.addCommand('@jrmc/adonis-attachment/commands') 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /docs/guide/advanced_usage/custom-converter.md: -------------------------------------------------------------------------------- 1 | # Custom converter 2 | 3 | ⚠️ [avalable in v2.4.0](/changelog#_2-4-0) 4 | 5 | ## Make converter 6 | 7 | The converts are stored within the ./app/converters directory. You may create a new converter by running the following command. 8 | 9 | ```sh 10 | node ace make:converter gif2webp 11 | ``` 12 | 13 | - Form: `singular` 14 | - Suffix: `converter` 15 | - Class name example: `Gif2WebpConverter` 16 | - File name example: `gif_2_webp_converter.ts` 17 | 18 | ## Samples animate gif to webp 19 | 20 | ```ts 21 | import type { ConverterAttributes } from '@jrmc/adonis-attachment/types/converter' 22 | import type { Input } from '@jrmc/adonis-attachment/types/input' 23 | 24 | import Converter from '@jrmc/adonis-attachment/converters/converter' 25 | import sharp from 'sharp' 26 | 27 | export default class Gif2WebpConverter extends Converter { 28 | async handle({ input }: ConverterAttributes): Promise { 29 | const sharpImage = sharp(input, { animated: true, pages: -1 }) 30 | 31 | const imageMeta = await sharpImage.metadata() 32 | const { loop, delay } = imageMeta 33 | 34 | const options = { 35 | webp: { 36 | loop, 37 | delay, 38 | } 39 | } 40 | 41 | const buffer = await sharpImage 42 | .withMetadata() 43 | .webp(options.webp) 44 | .toBuffer() 45 | 46 | return buffer 47 | } 48 | } 49 | ``` 50 | 51 | 52 | ## Samples video to animate gif 53 | 54 | ```ts 55 | import type { ConverterAttributes } from '@jrmc/adonis-attachment/types/converter' 56 | import type { Input } from '@jrmc/adonis-attachment/types/input' 57 | 58 | import os from 'node:os' 59 | import path from 'node:path' 60 | import fs from 'fs/promises' 61 | import { cuid } from '@adonisjs/core/helpers' 62 | import Converter from '@jrmc/adonis-attachment/converters/converter' 63 | import ffmpeg from 'fluent-ffmpeg' 64 | 65 | export default class Video2GifConverter extends Converter { 66 | async handle({ input }: ConverterAttributes): Promise { 67 | return await this.videoToGif(ffmpeg, input) 68 | } 69 | 70 | async videoToGif(ffmpeg: Function, input: Input) { 71 | let file = input 72 | 73 | if (Buffer.isBuffer(input)) { 74 | file = await this.bufferToTempFile(input) 75 | } 76 | 77 | return new Promise((resolve, reject) => { 78 | const folder = os.tmpdir() 79 | const filename = `${cuid()}.png` 80 | const destination = path.join(folder, filename) 81 | 82 | 83 | const ff = ffmpeg(file) 84 | 85 | if (this.binPaths) { 86 | if (this.binPaths.ffmpegPath) { 87 | ff.setFfmpegPath(this.binPaths.ffmpegPath) 88 | } 89 | } 90 | 91 | ff 92 | .withOptions([ 93 | '-ss 1', 94 | `-i ${file}`, 95 | `-filter_complex [0:v]trim=duration=3;`, 96 | '-f gif' 97 | ]) 98 | .on('end', () => { 99 | resolve(destination) 100 | }) 101 | .on('error', (err: Error) => { 102 | reject(err) 103 | }) 104 | .output(destination) 105 | .run() 106 | }) 107 | } 108 | 109 | async bufferToTempFile(input: Buffer) { 110 | const folder = os.tmpdir() 111 | const tempFilePath = path.join(folder, `tempfile-${Date.now()}.tmp`) 112 | await fs.writeFile(tempFilePath, input) 113 | return tempFilePath 114 | } 115 | } 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/guide/advanced_usage/exceptions.md: -------------------------------------------------------------------------------- 1 | 2 | # Exceptions 3 | 4 | |Code |Description | Origin | 5 | | -------------------------- | ----------------------------------------------- | ------- | 6 | | E_MISSING_PACKAGE | Missing package | | 7 | | E_CANNOT_CREATE_ATTACHMENT | Unable to create Attachment Object | | 8 | | E_CANNOT_CREATE_VARIANT | Unable to create variant | | 9 | | E_CANNOT_PATH_BY_CONVERTER | Missing path by converter | | 10 | | E_ISNOT_BUFFER | Is not a Buffer | | 11 | | E_ISNOT_BASE64 | Is not a Base64 | | 12 | | ENOENT | Unable to read file | | 13 | | E_CANNOT_WRITE_FILE | Unable to write file to the destination | Drive | 14 | | E_CANNOT_READ_FILE | Unable to read file | Drive | 15 | | E_CANNOT_DELETE_FILE | Unable to delete file | Drive | 16 | | E_CANNOT_SET_VISIBILITY | Unable to set file visibility | Drive | 17 | | E_CANNOT_GENERATE_URL | Unable to generate URL for a file | Drive | 18 | | E_UNALLOWED_CHARACTERS | The file key has unallowed set of characters | Drive | 19 | | E_INVALID_KEY | Key post normalization leads to an empty string | Drive | 20 | 21 | [Adonis documentation exception](https://docs.adonisjs.com/guides/basics/exception-handling) 22 | 23 | ## Handling exceptions 24 | 25 | If you want to handle a specific exception differently, you can do that inside the `handle` method. Make sure to use the `ctx.response.send` method to send a response, since the return value from the `handle` method is discarded. 26 | 27 | ::: code-group 28 | 29 | ```typescript [API] 30 | import { errors } from '@jrmc/adonis-attachment' 31 | 32 | export default class HttpExceptionHandler extends ExceptionHandler { 33 | async handle(error: unknown, ctx: HttpContext) { 34 | if (error instanceof errors.E_CANNOT_WRITE_FILE) { 35 | const err = error as errors.E_CANNOT_WRITE_FILE 36 | ctx.response.status(422).send(err.messages) 37 | return 38 | } 39 | 40 | return super.handle(error, ctx) 41 | } 42 | } 43 | ``` 44 | 45 | ```typescript [web] 46 | import { errors } from '@jrmc/adonis-attachment' 47 | 48 | export default class HttpExceptionHandler extends ExceptionHandler { 49 | async handle(error: unknown, ctx: HttpContext) { 50 | if (error instanceof errors.E_CANNOT_WRITE_FILE) { 51 | ctx.session.flash('notification', { 52 | type: 'error', 53 | message: err.message, 54 | }) 55 | 56 | return ctx.response.redirect('back') 57 | } 58 | 59 | return super.handle(error, ctx) 60 | } 61 | } 62 | 63 | ``` 64 | 65 | ::: 66 | -------------------------------------------------------------------------------- /docs/guide/advanced_usage/pre-compile-on-demand.md: -------------------------------------------------------------------------------- 1 | # Pre compute on demand 2 | 3 | We recommend not enabling the preComputeUrl option when you need the URL for just one or two queries and not within the rest of your application. 4 | 5 | For those couple of queries, you can manually compute the URLs within the controller. Here's a small helper method that you can drop on the model directly. 6 | 7 | ```ts 8 | import type { Attachment } from '@jrmc/adonis-attachment/types/attachment' 9 | import { attachment, attachmentManager } from '@jrmc/adonis-attachment' 10 | 11 | class User extends BaseModel { 12 | static async preComputeUrls(models: User | User[]) { 13 | if (Array.isArray(models)) { 14 | await Promise.all(models.map((model) => this.preComputeUrls(model))) 15 | return 16 | } 17 | 18 | // compute url for original file 19 | await attachmentManager.computeUrl(models.avatar) 20 | 21 | // compute url for thumbnail variant 22 | const thumb = models.avatar.getVariant('thumbnail') 23 | await attachmentManager.computeUrl(thumb) 24 | 25 | // compute url for medium variant with expiration time option 26 | const medium = models.avatar.getVariant('medium') 27 | await attachmentManager.computeUrl(medium, { 28 | expiresIn: '30 mins', 29 | }) 30 | } 31 | 32 | @attachment({ 33 | variants: ['thumbnail', 'medium', 'large'] 34 | }) 35 | declare avatar: Attachment 36 | } 37 | ``` 38 | 39 | computeUrl method create automatically creates a signed or unsigned url depending on Drive's configuration. 40 | 41 | it's possible to pass specific options to the signed url. 42 | options params accepts `expiresIn`, `contentType` et `contentDisposition`. 43 | 44 | [More informations](https://flydrive.dev/docs/disk_api#getsignedurl) 45 | 46 | --- 47 | 48 | And now use it as follows. 49 | 50 | ```ts 51 | const users = await User.all() 52 | await User.preComputeUrls(users) 53 | 54 | return users 55 | ``` 56 | 57 | Or for a single user 58 | 59 | ```ts 60 | const user = await User.findOrFail(1) 61 | await User.preComputeUrls(user) 62 | 63 | return user 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/guide/advanced_usage/queue.md: -------------------------------------------------------------------------------- 1 | # Queue 2 | 3 | The media transforms are carried out int the queue, grouped by model attribut with the [@poppinss/defer](https://github.com/poppinss/defer) library. 4 | 5 | ## Events 6 | 7 | Create your preload file for events catch: 8 | 9 | ```sh 10 | node ace make:preload queue 11 | ``` 12 | 13 | ```ts 14 | import { attachmentManager } from '@jrmc/adonis-attachment' 15 | import logger from '@adonisjs/core/services/logger' 16 | 17 | attachmentManager.queue.onError = function (error, task) { 18 | logger.info(`${task.name} task failed with the following error`) 19 | logger.error(error.message) 20 | } 21 | 22 | attachmentManager.queue.taskCompleted = function (task) { 23 | logger.info(`${task.name} completed. ${attachmentManager.queue.size()} tasks left`) 24 | } 25 | 26 | attachmentManager.queue.drained = function () { 27 | logger.info('Processed last task in the queue') 28 | } 29 | 30 | ``` 31 | 32 | ## Synchrone mode 33 | 34 | If you need to wait for all processes to complete, for example, for unit testing: 35 | 36 | 37 | ```ts 38 | const notifier = new Promise((resolve) => { 39 | attachmentManager.queue.drained = resolve 40 | }) 41 | 42 | generateUserWithAttachment() 43 | 44 | await notifier 45 | 46 | /** 47 | * Tasks have been processed. 48 | */ 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/guide/advanced_usage/regenerate-variant.md: -------------------------------------------------------------------------------- 1 | # Regeneration of variants 2 | 3 | You can regenerate the different variants using the `RegenerateService`. 4 | 5 | ⚠️ [change in v4.0.0](/changelog#_4-0-0) 6 | 7 | ## Regeneration by Row 8 | 9 | ```ts 10 | @inject() 11 | async regenerate(regenerate: RegenerateService) { 12 | const user = await User.first() 13 | 14 | if (user) { 15 | await regenerate.row(user).run() 16 | } 17 | } 18 | ``` 19 | 20 | ## Regeneration by Model 21 | 22 | ```ts 23 | @inject() 24 | async regenerate(regenerate: RegenerateService) { 25 | if (user) { 26 | await regenerate.model(User).run() 27 | } 28 | } 29 | ``` 30 | 31 | ## Options 32 | 33 | 34 | | Option | Description | 35 | | ---------- | ---------------------------- | 36 | | variants | Specify the variant names | 37 | | attributes | Specify the attribute names. | 38 | 39 | 40 | 41 | ```ts 42 | await regenerate.row(user, { 43 | variants: [ 'thumbnail' ], 44 | attributes: [ 'avatar', 'files' ] 45 | }).run() 46 | 47 | await regenerate.model(User, { 48 | variants: [ 'thumbnail' ], 49 | attributes: [ 'avatar', 'files' ] 50 | }).run() 51 | ``` 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/guide/basic_usage/attachment-object.md: -------------------------------------------------------------------------------- 1 | # Attachment Object 2 | 3 | The Attachment object is the main class that handles attached files in your application. 4 | 5 | ## Properties 6 | 7 | ### Base Properties 8 | - `drive`: Drive service used for file management 9 | - `name`: File name 10 | - `originalName`: Original file name 11 | - `folder`: Storage folder 12 | - `path`: Complete file path 13 | - `size`: File size in bytes 14 | - `extname`: File extension 15 | - `mimeType`: File MIME type 16 | - `meta`: File metadata (EXIF) 17 | - `originalPath`: Original file path 18 | - `url`: File URL 19 | - `variants`: Array of file variants 20 | 21 | ## Methods 22 | 23 | ### URL Management 24 | - `getUrl(variantName?: string): Promise`: Gets the file URL or a variant URL 25 | - `getSignedUrl(variantNameOrOptions?: string | SignedURLOptions, signedUrlOptions?: SignedURLOptions): Promise`: Gets a signed URL 26 | 27 | ### File Management 28 | - `getDisk(): Disk`: Gets the storage disk 29 | - `getBytes(): Promise`: Gets file content as bytes 30 | - `getBuffer(): Promise`: Gets file content as buffer 31 | - `getStream(): Promise`: Gets file content as stream 32 | 33 | ## Usage Examples 34 | 35 | ### URL Management 36 | ```typescript 37 | // Getting the URL 38 | const url = await attachment.getUrl(); 39 | 40 | // Getting a variant URL 41 | const variantUrl = await attachment.getUrl('thumbnail'); 42 | 43 | // Getting a signed URL 44 | const signedUrl = await attachment.getSignedUrl({ 45 | expiresIn: '30m' 46 | }); 47 | 48 | // Getting a signed URL for a variant 49 | const signedVariantUrl = await attachment.getSignedUrl('thumbnail', { 50 | expiresIn: '30m' 51 | }); 52 | ``` 53 | 54 | ### File Management 55 | ```typescript 56 | // Getting the storage disk 57 | const disk = attachment.getDisk(); 58 | 59 | // Getting file content as bytes 60 | const bytes = await attachment.getBytes(); 61 | 62 | // Getting file content as buffer 63 | const buffer = await attachment.getBuffer(); 64 | 65 | // Getting file content as stream 66 | const stream = await attachment.getStream(); 67 | stream.pipe(fs.createWriteStream('output.jpg')); 68 | ``` -------------------------------------------------------------------------------- /docs/guide/basic_usage/migration-setup.md: -------------------------------------------------------------------------------- 1 | # Migration setup 2 | 3 | Often times, the size of the image metadata could exceed the allowable length of an SQL `String` data type. So, it is recommended to create/modify the column which will hold the metadata to use a `JSON` data type. 4 | 5 | If you are creating the column for the first time, make sure that you use the JSON data type. Example: 6 | 7 | ```ts 8 | // Within the migration file 9 | 10 | protected tableName = 'users' 11 | 12 | public async up() { 13 | this.schema.createTable(this.tableName, (table) => { 14 | table.increments() 15 | table.json('avatar') // <-- Use a JSON data type 16 | }) 17 | } 18 | ``` 19 | 20 | If you already have a column for storing image paths/URLs, you need to create a new migration and alter the column definition to a JSON data type. Example: 21 | 22 | ```bash 23 | # Create a new migration file 24 | node ace make:migration change_avatar_column_to_json --table=users 25 | ``` 26 | 27 | ```ts 28 | // Within the migration file 29 | 30 | protected tableName = 'users' 31 | 32 | public async up() { 33 | this.schema.alterTable(this.tableName, (table) => { 34 | table.json('avatar').alter() // <-- Alter the column definition 35 | }) 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/guide/basic_usage/view-setup.md: -------------------------------------------------------------------------------- 1 | # View setup 2 | 3 | Now all you have to do is display your images in your view. 4 | 5 | ## URLs for edge template 6 | 7 | ```ts 8 | await user.avatar.getUrl() 9 | await user.avatar.getUrl('thumbnail') 10 | // or await user.avatar.getVariant('thumbnail').getUrl() 11 | 12 | await user.avatar.getSignedUrl() 13 | await user.avatar.getSignedUrl('thumbnail') 14 | // or await user.avatar.getVariant('thumbnail').getSignedUrl() 15 | ``` 16 | 17 | ```edge 18 | 19 | 20 | 21 | 24 | 25 | 26 | 29 | ``` 30 | 31 | getSignedUrl options params accepts `expiresIn`, `contentType` et `contentDisposition`. [More informations](https://flydrive.dev/docs/disk_api#getsignedurl) 32 | 33 | ### If preComputeUrl is enabled 34 | 35 | ```edge 36 | 37 | 38 | ``` 39 | 40 | 41 | ## URLs for Inertia template 42 | 43 | ::: code-group 44 | ```js [react] 45 | 46 | ``` 47 | 48 | ```vue 49 | 50 | ``` 51 | 52 | ```svelte 53 | 54 | ``` 55 | ::: 56 | 57 | preComputeUrl is required. 58 | -------------------------------------------------------------------------------- /docs/guide/converters/document-thumbnail.md: -------------------------------------------------------------------------------- 1 | 2 | # Document thumbnail converter 3 | 4 | ⚠️ [avalable in v2.3.0](/changelog#_2-3-0) 5 | 6 | 7 | 8 | ## Configuration 9 | 10 | ```typescript 11 | // config/attachment.ts // [!code focus:1] 12 | const attachmentConfig = defineConfig({ 13 | converters: { 14 | preview: { // [!code focus:3] 15 | converter: () => import('@jrmc/adonis-attachment/converters/document_thumbnail_converter'), 16 | } 17 | } 18 | }) 19 | ``` 20 | 21 | By default, image format is `JPEG` and size is video size. `options` attribute use ***[image_converter](/guide/converters/image)*** 22 | 23 | Sample: 24 | 25 | ```typescript{6-9} 26 | const attachmentConfig = defineConfig({ 27 | converters: { 28 | preview: { // [!code focus:7] 29 | converter: () => import('@jrmc/adonis-attachment/converters/document_thumbnail_converter'), 30 | options: { 31 | format: 'webp', 32 | resize: 720 33 | } 34 | } 35 | } 36 | }) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/guide/converters/image.md: -------------------------------------------------------------------------------- 1 | # Image converter 2 | 3 | 4 | 5 | ## Configuration 6 | 7 | ```typescript 8 | // config/attachment.ts 9 | const attachmentConfig = defineConfig({ 10 | converters: { 11 | large: { // [!code focus:6] 12 | converter: () => import('@jrmc/adonis-attachment/converters/image_converter'), 13 | options: { 14 | resize: 1280, 15 | } 16 | } 17 | } 18 | }) 19 | ``` 20 | 21 | ## Format 22 | 23 | The default format is `webp`, for change, use options format: 24 | 25 | ```typescript 26 | const attachmentConfig = defineConfig({ 27 | converters: { 28 | thumbnail: { // [!code focus:7] 29 | converter: () => import('@jrmc/adonis-attachment/converters/image_converter'), 30 | options: { 31 | resize: 300, 32 | format: 'jpeg', // [!code highlight] 33 | } 34 | } 35 | } 36 | }) 37 | ``` 38 | 39 | Options format is `string` or `object` [ format, options ] details in documentation : [sharp api outpout](https://sharp.pixelplumbing.com/api-output#toformat) 40 | 41 | 42 | Sample for personalize image quality: 43 | 44 | ```typescript{8-13} 45 | const attachmentConfig = defineConfig({ 46 | converters: { 47 | thumbnail: { // [!code focus:12] 48 | converter: () => import('@jrmc/adonis-attachment/converters/image_converter'), 49 | options: { 50 | resize: 300, 51 | format: { 52 | format: 'jpeg', 53 | options: { 54 | quality: 80 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }) 61 | ``` 62 | 63 | ## ReSize 64 | 65 | Options resize is `number` or `object`(options) details in documentation : [sharp api resize](https://sharp.pixelplumbing.com/api-resize) 66 | 67 | Sample: 68 | 69 | ```typescript{11-16} 70 | import { defineConfig } from '@jrmc/adonis-attachment' 71 | import { InferConverters } from '@jrmc/adonis-attachment/types/config' 72 | import sharp from 'sharp' 73 | 74 | const attachmentConfig = defineConfig({ 75 | converters: { 76 | thumbnail: { 77 | converter: () => import('@jrmc/adonis-attachment/converters/image_converter'), 78 | options: { 79 | format: 'jpeg', 80 | resize: { // https://sharp.pixelplumbing.com/api-resize 81 | width: 400, 82 | height: 400, 83 | fit: sharp.fit.cover, 84 | position: 'top' 85 | }, 86 | } 87 | } 88 | } 89 | }) 90 | 91 | export default attachmentConfig 92 | 93 | declare module '@jrmc/adonis-attachment' { 94 | interface AttachmentVariants extends InferConverters {} 95 | } 96 | ``` 97 | 98 | ## BlurHash 99 | 100 | The blurhash option is used to enable, disable, and customise the generation of blurhashes ([https://blurha.sh/](https://blurha.sh/)) for the variants. Blurhash generation is disabled by default. 101 | 102 | ```typescript 103 | const attachmentConfig = defineConfig({ 104 | converters: { 105 | thumbnail: { // [!code focus:10] 106 | converter: () => import('@jrmc/adonis-attachment/converters/image_converter'), 107 | options: { 108 | blurhash: true 109 | // or 110 | // blurhash: { 111 | // enabled: true, 112 | // componentX: 4, 113 | // componentY: 4 114 | // } 115 | } 116 | } 117 | } 118 | }) 119 | ``` 120 | 121 | For more about componentX and componentY properties read [here](https://github.com/woltapp/blurhash?tab=readme-ov-file#how-do-i-pick-the-number-of-x-and-y-components). 122 | -------------------------------------------------------------------------------- /docs/guide/converters/pdf-thumbnail.md: -------------------------------------------------------------------------------- 1 | 2 | # PDF thumbnail converter 3 | 4 | ⚠️ [avalable in v2.3.0](/changelog#_2-3-0) 5 | 6 | 7 | 8 | ## Configuration 9 | 10 | ```typescript 11 | // config/attachment.ts // [!code focus:1] 12 | const attachmentConfig = defineConfig({ 13 | converters: { 14 | preview: { // [!code focus:3] 15 | converter: () => import('@jrmc/adonis-attachment/converters/pdf_thumbnail_converter'), 16 | } 17 | } 18 | }) 19 | ``` 20 | 21 | By default, image format is `JPEG` and size is video size. `options` attribute use ***[image_converter](/guide/converters/image)*** 22 | 23 | Sample: 24 | 25 | ```typescript{6-9} 26 | const attachmentConfig = defineConfig({ 27 | converters: { 28 | preview: { // [!code focus:7] 29 | converter: () => import('@jrmc/adonis-attachment/converters/pdf_thumbnail_converter'), 30 | options: { 31 | format: 'webp', 32 | resize: 720 33 | } 34 | } 35 | } 36 | }) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/guide/converters/video-thumbnail.md: -------------------------------------------------------------------------------- 1 | 2 | # Video thumbnail converter 3 | 4 | 5 | 6 | 7 | ## Configuration 8 | 9 | ```typescript 10 | // config/attachment.ts // [!code focus:1] 11 | const attachmentConfig = defineConfig({ 12 | converters: { 13 | preview: { // [!code focus:3] 14 | converter: () => import('@jrmc/adonis-attachment/converters/video_thumbnail_converter'), 15 | } 16 | } 17 | }) 18 | ``` 19 | 20 | By default, image format is `PNG` and size is video size. `options` attribute use ***[image_converter](/guide/converters/image)*** 21 | 22 | Sample: 23 | 24 | ```typescript{6-9} 25 | const attachmentConfig = defineConfig({ 26 | converters: { 27 | preview: { // [!code focus:7] 28 | converter: () => import('@jrmc/adonis-attachment/converters/video_thumbnail_converter'), 29 | options: { 30 | format: 'jpeg', 31 | resize: 720 32 | } 33 | } 34 | } 35 | }) 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/guide/essentials/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The Adonis Attachment package is available on [npm](https://www.npmjs.com/package/@jrmc/adonis-attachment). 4 | 5 | It's required [Adonis Drive](https://docs.adonisjs.com/guides/digging-deeper/drive), please look at the documentation if this is not installed. 6 | 7 | You can install it using the following ace command to automagically configure it: 8 | ```sh 9 | node ace add @jrmc/adonis-attachment 10 | ``` 11 | 12 | Alternatively, you can install it manually using your favorite package manager and running the configure command: 13 | ::: code-group 14 | 15 | ```sh [npm] 16 | npm install @jrmc/adonis-attachment 17 | node ace configure @jrmc/adonis-attachment 18 | ``` 19 | ```sh [pnpm] 20 | pnpm install @jrmc/adonis-attachment 21 | node ace configure @jrmc/adonis-attachment 22 | ``` 23 | ```sh [yarn] 24 | yarn add @jrmc/adonis-attachment 25 | node ace configure @jrmc/adonis-attachment 26 | ``` 27 | ::: 28 | 29 | 30 | ## Additional install 31 | 32 | 33 | 34 | --- 35 | 36 | 37 | 38 | --- 39 | 40 | 41 | 42 | --- 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/guide/essentials/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The `adonis-attachment` package was designed to simplify file upload management. It allows you to create alternative files, called `variants`, with options for optimization, resizing, and format changes. Additionally, it automatically generates image thumbnails for document and video files. 4 | 5 | The creation of variants is handled through Converters. 6 | 7 | 8 | 9 | Project sample : [adonis-starter-kit](https://github.com/batosai/adonis-starter-kit) 10 | -------------------------------------------------------------------------------- /docs/guide/partials/install-document.md: -------------------------------------------------------------------------------- 1 | Variants images for thumbnail Document are generates by [libreoffice-file-converter](https://www.npmjs.com/package/libreoffice-file-converter). Make sure you have [LibreOffice](https://fr.libreoffice.org/download/telecharger-libreoffice/) installed on your system. 2 | 3 | It is possible to specify the [path of binaries](/guide/essentials/configuration.html#bin-optional). Useful if your installations are specific or if you are dropping off precompiled versions. 4 | 5 | Installation required: 6 | 7 | ::: code-group 8 | ```sh [npm] 9 | npm install libreoffice-file-converter 10 | ``` 11 | ```sh [pnpm] 12 | pnpm install libreoffice-file-converter 13 | ``` 14 | ```sh [yarn] 15 | yarn add libreoffice-file-converter 16 | ``` 17 | ::: 18 | -------------------------------------------------------------------------------- /docs/guide/partials/install-image.md: -------------------------------------------------------------------------------- 1 | Variants images are generates by [sharp module](https://sharp.pixelplumbing.com) and require installation: 2 | 3 | ::: code-group 4 | ```sh [npm] 5 | npm install sharp 6 | ``` 7 | ```sh [pnpm] 8 | pnpm install sharp 9 | ``` 10 | ```sh [yarn] 11 | yarn add sharp 12 | ``` 13 | ::: 14 | -------------------------------------------------------------------------------- /docs/guide/partials/install-pdf.md: -------------------------------------------------------------------------------- 1 | Variants images for thumbnail PDF are generates by [node-poppler](https://www.npmjs.com/package/node-poppler). Make sure you have [poppler](https://poppler.freedesktop.org)(pdftocairo) installed on your system. 2 | 3 | It is possible to specify the [path of binaries](/guide/essentials/configuration.html#bin-optional). Useful if your installations are specific or if you are dropping off precompiled versions. 4 | 5 | Installation required: 6 | 7 | ::: code-group 8 | ```sh [npm] 9 | npm install node-poppler 10 | ``` 11 | ```sh [pnpm] 12 | pnpm install node-poppler 13 | ``` 14 | ```sh [yarn] 15 | yarn add node-poppler 16 | ``` 17 | ::: 18 | -------------------------------------------------------------------------------- /docs/guide/partials/install-video.md: -------------------------------------------------------------------------------- 1 | Variants images for thumbnail video are generates by [fluent-ffmpeg](https://www.npmjs.com/package/fluent-ffmpeg). Make sure you have [ffmpeg](https://ffmpeg.org) installed on your system (including all necessary encoding libraries like libmp3lame or libx264). 2 | 3 | It is possible to specify the [path of binaries](/guide/essentials/configuration.html#bin-optional). Useful if your installations are specific or if you are dropping off precompiled versions. 4 | 5 | Installation required: 6 | 7 | ::: code-group 8 | ```sh [npm] 9 | npm install fluent-ffmpeg 10 | ``` 11 | ```sh [pnpm] 12 | pnpm install fluent-ffmpeg 13 | ``` 14 | ```sh [yarn] 15 | yarn add fluent-ffmpeg 16 | ``` 17 | ::: 18 | -------------------------------------------------------------------------------- /docs/guide/partials/table-converter.md: -------------------------------------------------------------------------------- 1 | |Converter |File type |Description|Required | 2 | | ------------- | :-----------: | -------- | -------- | 3 | |[image_converter](/guide/converters/image)|JPEG, PNG, WebP, GIF and AVIF | Generate other image and change format/size etc...|[sharp](https://sharp.pixelplumbing.com/)| 4 | |[pdf_thumbnail_converter](/guide/converters/pdf-thumbnail)|PDF |Generate thumbnail image of PDF|[poppler(pdftocairo)](https://www.npmjs.com/package/node-poppler)| 5 | |[document_thumbnail_converter](/guide/converters/document-thumbnail)|PDF, ODT, ODS, DOCX, DOC, NUMBERS, PAGES, XLSX, XLS, CSV, RTF, TXT |Generate thumbnail image of document|[libreoffice-file-converter](https://www.npmjs.com/package/libreoffice-file-converter)| 6 | |[video_thumbnail_converter](/guide/converters/video-thumbnail)|MP4, MOV, AVI, FLV, MKV |Generate thumbnail image of video|[fluent-ffmpeg](https://www.npmjs.com/package/fluent-ffmpeg)| 7 | -------------------------------------------------------------------------------- /docs/guide/start-here.md: -------------------------------------------------------------------------------- 1 | # Start Here 2 | 3 | Simple upload file 4 | 5 | ```ts 6 | // app/models/user.ts 7 | import { BaseModel } from '@adonisjs/lucid/orm' 8 | import { attachment } from '@jrmc/adonis-attachment' 9 | import type { Attachment } from '@jrmc/adonis-attachment/types/attachment' // [!code highlight] 10 | 11 | class User extends BaseModel { 12 | @attachment() // [!code highlight] 13 | declare avatar: Attachment // [!code highlight] 14 | } 15 | ``` 16 | 17 | --- 18 | 19 | ```ts 20 | // app/controllers/users_controller.ts 21 | import { attachmentManager } from '@jrmc/adonis-attachment' // [!code focus] 22 | 23 | class UsersController { 24 | public store({ request }: HttpContext) { 25 | const avatar = request.file('avatar')! // [!code focus] 26 | const user = new User() 27 | 28 | user.avatar = await attachmentManager.createFromFile(avatar) // [!code focus] 29 | await user.save() 30 | } 31 | } 32 | ``` 33 | 34 | --- 35 | 36 | ```edge 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/guide/use-cases/picture.md: -------------------------------------------------------------------------------- 1 | # Picture 2 | 3 | Using your variants with `` for create a component. 4 | 5 | ::: code-group 6 | 7 | ```js [edge] 8 | // picture.edge 9 | 10 | 11 | 12 | 13 | image description 14 | 15 | ``` 16 | ```js [react] 17 | // picture.jsx 18 | import React from 'react'; 19 | 20 | const Picture = ({ source, alt }) => { 21 | return ( 22 | 23 | 24 | 25 | 26 | {alt} 27 | 28 | ) 29 | } 30 | ``` 31 | ```svelte [vue] 32 | // picture.vue 33 |