Attachment Lite
一个简单的、自以为是的包,用于转换任何列在您的 Lucid 模型上添加到附件数据类型。
Attachment lite 允许您在数据库中存储用户上传文件的引用。 它不需要任何额外的数据库表,并将文件元数据作为 JSON 存储在同一列中。
How it works?
attachment-lite
包是媒体库方法的替代方法。 我相信媒体库在创建需要一个中心位置来保存所有图像/文档的 CMS 时非常有用。
但是,许多应用程序(如 SAAS 产品或社区论坛)不需要媒体库。
例如,像 Twitter 或 dev.to 这样的网站没有媒体库部分,您可以在其中上传和选择图像。 相反,这些平台上的图像与资源紧密结合。
当您在 Twitter 上更新个人资料图片时,旧图片会消失,新图片会出现。 没有中央图片库可供选择个人资料图片。
说来话长,attachment-lite
包是管理应用程序中一次性文件上传的绝佳解决方案。
Features
- Turn any column in your database to an attachment data type.
- No additional database tables are required. The file metadata is stored as JSON within the same column.
- Automatically removes the old file from the disk when a new file is assigned.
- Handles failure cases gracefully. No files will be stored if the model fails to persist.
- Similarly, no old files are removed if the model fails to persist during an update or the deletion fails.
Pre-requisites
attachment-lite
包需要 @adonisjs/lucid >= v16.3.1
和 @adonisjs/core >= 5.3.4
。
此外,它依赖于 AdonisJS 驱动器 在磁盘上写入文件。
Setup
如下所示从 npm 注册表安装包。
npm i @adonisjs/attachment-lite
接下来,通过运行以下 ace 命令来配置包。
node ace configure @adonisjs/attachment-lite
Usage
通常,图像元数据的大小可能会超过 SQL String
数据类型的允许长度。 因此,建议创建/修改将保存元数据的列以使用 JSON
数据类型。
如果您是第一次创建列,请确保使用 JSON 数据类型。 示例:
// Within the migration file
protected tableName = 'users'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments()
table.json('avatar') // <-- Use a JSON data type
})
}
如果您已经有一个用于存储图像路径/URL 的列,则需要创建一个新的迁移并将列定义更改为 JSON 数据类型。 示例:
# Create a new migration file
node ace make:migration change_avatar_column_to_json --table=users
// Within the migration file
protected tableName = 'users'
public async up() {
this.schema.alterTable(this.tableName, (table) => {
table.json('avatar').alter() // <-- Alter the column definition
})
}
接下来,在模型中,从包中导入 attachment
装饰器和 AttachmentContract
接口。
确保在使用 @attachment
装饰器时不要使用 @column
装饰器。
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import {
attachment,
AttachmentContract
} from '@ioc:Adonis/Addons/AttachmentLite'
class User extends BaseModel {
@attachment()
public avatar: AttachmentContract
}
现在您可以从用户上传的文件创建附件,如下所示。
import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'
class UsersController {
public store({ request }: HttpContextContract) {
const avatar = request.file('avatar')!
const user = new User()
user.avatar = Attachment.fromFile(avatar)
await user.save()
}
}
Attachment.fromFile
从用户上传的文件创建附件类的实例。 当您将模型保存到数据库时,attachment-lite 会将文件写入磁盘。
Handling updates
您可以使用新上传的用户文件更新属性,程序包将负责删除旧文件并存储新文件。
import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'
class UsersController {
public update({ request }: HttpContextContract) {
const user = await User.firstOrFail()
const avatar = request.file('avatar')!
user.avatar = Attachment.fromFile(avatar)
// Old file will be removed from the disk as well.
await user.save()
}
}
同样,将 null
值分配给模型属性以删除文件而不分配新文件。
此外,请确保将模型上的属性类型也更新为 null
。
class User extends BaseModel {
@attachment()
public avatar: AttachmentContract | null
}
const user = await User.first()
user.avatar = null
// Removes the file from the disk
await user.save()
Handling deletes
删除模型实例后,所有相关附件都将从磁盘中删除。
请注意:对于附件精简版删除文件,您必须使用 modelInstance.delete
方法。 在查询构建器上使用 delete
将不起作用。
const user = await User.first()
// Removes any attachments related to this user
await user.delete()
Specifying disk
默认情况下,所有文件都从默认磁盘写入/删除。 但是,您可以在使用 attachment
装饰器时指定自定义磁盘。
disk
属性值永远不会保存到数据库中。 这意味着,如果您首先将磁盘定义为 s3
,上传一些文件,然后将磁盘值更改为 gcs
,程序包将使用 查找文件gcs
磁盘。
class User extends BaseModel {
@attachment({ disk: 's3' })
public avatar: AttachmentContract
}
Specifying subfolder
您还可以通过定义 folder
属性将文件存储在子文件夹中,如下所示。
class User extends BaseModel {
@attachment({ folder: 'avatars' })
public avatar: AttachmentContract
}
Generating URLs
您可以使用 getUrl
或 getSignedUrl
方法为给定的附件生成 URL。 它们与驱动方法相同,只是您不必指定文件名。
await user.avatar.getSignedUrl({ expiresIn: '30mins' })
Generating URLs for the API response
用于生成 URL 的 Drive API 方法是异步的,而将模型序列化为 JSON 是同步的。 因此,它不是在序列化模型时创建 URL。
// ❌ Does not work
const users = await User.all()
users.map((user) => {
user.avatar.url = await user.avatar.getSignedUrl()
return user
})
要解决此用例,您可以选择预计算 URL
Pre compute URLs
启用 preComputeUrl
标志以在 SELECT 查询后预计算 URL。 例如:
class User extends BaseModel {
@attachment({ preComputeUrl: true })
public avatar: AttachmentContract
}
Fetch result
const users = await User.all()
users[0].avatar.url // pre computed already
Find result
const user = await User.findOrFail(1)
user.avatar.url // pre computed already
Pagination result
const users = await User.query.paginate(1)
users[0].avatar.url // pre computed already
preComputeUrl
属性将生成 URL 并将其设置在 Attachment 类实例上。 此外,当磁盘为私有 时生成签名 URL,当磁盘为公共 时生成普通 URL。
Pre compute on demand
当您只需要一个或两个查询的 URL 而不是在您的应用程序的其余部分中时,我们建议不要启用 preComputeUrl
选项。
对于这对查询,您可以在控制器中手动计算 URL。 这是一个可以直接放在模型上的小辅助方法。
class User extends BaseModel {
public static async preComputeUrls(models: User | User[]) {
if (Array.isArray(models)) {
await Promise.all(models.map((model) => this.preComputeUrls(model)))
return
}
await models.avatar?.computeUrl()
await models.coverImage?.computeUrl()
}
}
现在按如下方式使用它。
const users = await User.all()
await User.preComputeUrls(users)
return users
或者对于单个用户
const user = await User.findOrFail(1)
await User.preComputeUrls(user)
return user
Attachment Lite
A simple, opinionated package to convert any column on your Lucid model to an attachment data type.
Attachment lite allows you to store a reference of user uploaded files within the database. It does not require any additional database tables and stores the file metadata as JSON within the same column.
How it works?
The attachment-lite
package is an alternative to the media library approach. I believe media libraries are great when creating a CMS that wants a central place to keep all the images/documents.
However, many applications like a SAAS product or a community forum do not need media libraries.
For example, websites like Twitter or dev.to don't have a media library section where you upload and choose images from. Instead, images on these platforms are tightly coupled with the resource.
When you update your profile image on Twitter, the old image disappears, and the new one appears. There is no central gallery of images to choose the profile picture from.
A very long story to tell you that the attachment-lite
package is an excellent solution for managing one-off file uploads in your application.
Features
- Turn any column in your database to an attachment data type.
- No additional database tables are required. The file metadata is stored as JSON within the same column.
- Automatically removes the old file from the disk when a new file is assigned.
- Handles failure cases gracefully. No files will be stored if the model fails to persist.
- Similarly, no old files are removed if the model fails to persist during an update or the deletion fails.
Pre-requisites
The attachment-lite
package requires @adonisjs/lucid >= v16.3.1
and @adonisjs/core >= 5.3.4
.
Also, it relies on AdonisJS drive for writing files on the disk.
Setup
Install the package from the npm registry as follows.
npm i @adonisjs/attachment-lite
Next, configure the package by running the following ace command.
node ace configure @adonisjs/attachment-lite
Usage
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.
If you are creating the column for the first time, make sure that you use the JSON data type. Example:
// Within the migration file
protected tableName = 'users'
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments()
table.json('avatar') // <-- Use a JSON data type
})
}
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:
# Create a new migration file
node ace make:migration change_avatar_column_to_json --table=users
// Within the migration file
protected tableName = 'users'
public async up() {
this.schema.alterTable(this.tableName, (table) => {
table.json('avatar').alter() // <-- Alter the column definition
})
}
Next, in the model, import the attachment
decorator and the AttachmentContract
interface from the package.
Make sure NOT to use the @column
decorator when using the @attachment
decorator.
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import {
attachment,
AttachmentContract
} from '@ioc:Adonis/Addons/AttachmentLite'
class User extends BaseModel {
@attachment()
public avatar: AttachmentContract
}
Now you can create an attachment from the user uploaded file as follows.
import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'
class UsersController {
public store({ request }: HttpContextContract) {
const avatar = request.file('avatar')!
const user = new User()
user.avatar = Attachment.fromFile(avatar)
await user.save()
}
}
The Attachment.fromFile
creates an instance of the Attachment class from the user uploaded file. When you persist the model to the database, the attachment-lite will write the file to the disk.
Handling updates
You can update the property with a newly uploaded user file, and the package will take care of removing the old file and storing the new one.
import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'
class UsersController {
public update({ request }: HttpContextContract) {
const user = await User.firstOrFail()
const avatar = request.file('avatar')!
user.avatar = Attachment.fromFile(avatar)
// Old file will be removed from the disk as well.
await user.save()
}
}
Similarly, assign null
value to the model property to delete the file without assigning a new file.
Also, make sure you update the property type on the model to be null
as well.
class User extends BaseModel {
@attachment()
public avatar: AttachmentContract | null
}
const user = await User.first()
user.avatar = null
// Removes the file from the disk
await user.save()
Handling deletes
Upon deleting the model instance, all the related attachments will be removed from the disk.
Do note: For attachment lite to delete files, you will have to use the modelInstance.delete
method. Using delete
on the query builder will not work.
const user = await User.first()
// Removes any attachments related to this user
await user.delete()
Specifying disk
By default, all files are written/deleted from the default disk. However, you can specify a custom disk at the time of using the attachment
decorator.
The disk
property value is never persisted to the database. It means, if you first define the disk as s3
, upload a few files and then change the disk value to gcs
, the package will look for files using the gcs
disk.
class User extends BaseModel {
@attachment({ disk: 's3' })
public avatar: AttachmentContract
}
Specifying subfolder
You can also store files inside the subfolder by defining the folder
property as follows.
class User extends BaseModel {
@attachment({ folder: 'avatars' })
public avatar: AttachmentContract
}
Generating URLs
You can generate a URL for a given attachment using the getUrl
or getSignedUrl
methods. They are identical to the Drive methods, just that you don't have to specify the file name.
await user.avatar.getSignedUrl({ expiresIn: '30mins' })
Generating URLs for the API response
The Drive API methods for generating URLs are asynchronous, whereas serializing a model to JSON is synchronous. Therefore, it is not to create URLs at the time of serializing a model.
// ❌ Does not work
const users = await User.all()
users.map((user) => {
user.avatar.url = await user.avatar.getSignedUrl()
return user
})
To address this use case, you can opt for pre-computing URLs
Pre compute URLs
Enable the preComputeUrl
flag to pre compute the URLs after SELECT queries. For example:
class User extends BaseModel {
@attachment({ preComputeUrl: true })
public avatar: AttachmentContract
}
Fetch result
const users = await User.all()
users[0].avatar.url // pre computed already
Find result
const user = await User.findOrFail(1)
user.avatar.url // pre computed already
Pagination result
const users = await User.query.paginate(1)
users[0].avatar.url // pre computed already
The preComputeUrl
property will generate the URL and set it on the Attachment class instance. Also, a signed URL is generated when the disk is private, and a normal URL is generated when the disk is public.
Pre compute on demand
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.
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.
class User extends BaseModel {
public static async preComputeUrls(models: User | User[]) {
if (Array.isArray(models)) {
await Promise.all(models.map((model) => this.preComputeUrls(model)))
return
}
await models.avatar?.computeUrl()
await models.coverImage?.computeUrl()
}
}
And now use it as follows.
const users = await User.all()
await User.preComputeUrls(users)
return users
Or for a single user
const user = await User.findOrFail(1)
await User.preComputeUrls(user)
return user