云存储
AdonisJS Drive 是云存储服务之上的抽象,例如:Amazon S3、DigitalOcean Spaces 和 Google Cloud Storage。
Drive 与框架的核心预先捆绑在一起,因此不需要额外的安装步骤(驱动程序除外)。你可以按如下方式使用云端硬盘:
import Drive from '@ioc:Adonis/Core/Drive'
// 写一个文件
await Drive.put(filePath, stringOrBuffer)
await Drive.putStream(filePath, readableStream)
// 读取一个文件
const contents = await Drive.get(filePath)
const readableStream = await Drive.getStream(filePath)
// 查找文件是否存在
if (await Drive.exists(filePath)) {
await Drive.get(filePath)
}
目标和设计限制
Drive 的主要目标是提供适用于所有存储提供商的一致 API。因此,例如,你可以在开发期间使用本地文件系统,并在生产中切换到S3,而无需更改任何一行代码。
为了保证 API 的一致性,Drive 无法使用给定存储服务的细节。
例如,您无法使用 Drive 创建符号链接,因为 symlinks 是基于 Unix 的文件系统概念,无法使用 S3 或 GCS 进行复制。
同样,也不支持无法跨驱动程序复制的云服务的专有功能。
用例
Drive 不能替代管理您的网站静态资产(如 CSS、JavaScript 或用于设计网站/Web 应用程序的图像/图标)。
Drive 的主要用例是帮助你快速管理用户上传的文件。这些可以是用户头像、博客文章封面图片或任何其他运行时管理的文档。
配置
Drive 的配置存储在 config/drive.ts
文件中。在此文件中,你可以使用相同/不同的驱动程序定义多个磁盘。
随意使用 config stub 创建配置文件(如果缺少)。
// 文件名: config/drive.ts
import { driveConfig } from '@adonisjs/core/build/config'
export default driveConfig({
disk: Env.get('DRIVE_DISK'),
disks: {
local: {
driver: 'local',
visibility: 'public',
root: Application.tmpPath('uploads'),
basePath: '/uploads',
serveAssets: true,
},
s3: {
driver: 's3',
visibility: 'public',
key: Env.get('S3_KEY'),
secret: Env.get('S3_SECRET'),
region: Env.get('S3_REGION'),
bucket: Env.get('S3_BUCKET'),
endpoint: Env.get('S3_ENDPOINT'),
// 让 minio 工作
// forcePathStyle: true,
},
},
})
disk
disks
对象定义了你要在整个应用程序中使用的磁盘。每个磁盘都必须指定要使用的驱动程序。
disks
disks
对象定义了你要在整个应用程序中使用的磁盘。每个磁盘都必须指定要使用的驱动程序。
驱动
以下是官方驱动程序列表。
本地驱动
local
驱动程序已预先捆绑到框架核心中。它使用本地文件系统来读取/写入文件。
你必须在配置文件中配置本地驱动程序的根目录。该路径可以在你计算机上的任何位置(即使在项目根目录之外也可以)。
local: {
driver: 'local',
root: Application.tmpPath('uploads'),
},
为了模仿云服务的行为,本地驱动程序还可以在定义 basePath
并启用 serveFiles
选项时提供文件。
备注:
确保你没有在应用程序中使用与basePath
相同的前缀定义任何其他路由。
local: {
basePath: '/uploads',
serveAssets: true,
}
配置完成后,Drive.getUrl
方法将生成下载文件的 URL。 URL 是相对于当前域的。
await Drive.getUrl('avatar.jpg')
// 返回
// /uploads/avatar.jpg
await Drive.getSignedUrl('avatar.jpg')
// 返回
// /uploads/avatar.jpg?signature=eyJtZXNzYWdlIjoiL3YxL3VzZXJzIn0.CGHY99jESI-AxPFBu1lE26TXjCASfC83XTyu58NivFw
S3 驱动
s3
驱动程序利用 Amazon S3 云存储来读取/写入文件。你必须单独安装驱动程序。
确保按照 configure
命令说明正确设置驱动程序。你还可以阅读说明 说明。
npm i @adonisjs/drive-s3
node ace configure @adonisjs/drive-s3
你还可以将 s3
驱动程序与 S3 兼容的服务一起使用,例如 DigitalOcean Spaces 和 [MinIO](min. io/)。
使用不同的服务时,你还必须定义存储桶端点。
{
driver: 's3',
endpoint: Env.get('S3_ENDPOINT')
}
GCS 驱动
gcs
驱动程序使用 Google Cloud Storage 来读取/写入文件。你必须单独安装驱动程序。
确保按照 configure
命令说明正确设置驱动程序。你还可以阅读 相同的说明。
npm i @adonisjs/drive-gcs
node ace configure @adonisjs/drive-gcs
如果你使用 GCS 统一 ACL,请确保将 usingUniformAcl
选项设置为 true。
文件可见性
Drive 允许你以 public
或 private
可见性保存文件。公共文件可以使用文件 URL 访问,而私有文件可以在服务器上读取或使用签名 URL 访问。
你可以通过在配置文件中定义 visibility
选项来配置整个磁盘的可见性。
{
disks: {
local: {
driver: 'local',
visibility: 'private'
// ... 其余的配置
}
}
}
s3
和 gcs
驱动程序还允许你定义单个文件的可见性。但是,出于以下原因,我们建议对公共文件和私有文件使用单独的存储桶。
- 使用单独的 bucket 时,可以在整个 bucket 上配置一个 CDN 来服务公共文件。
- 获得
local
文件驱动程序更好的交叉兼容性,因为本地驱动程序不允许文件级别的可见性控制。
无论使用何种驱动程序,你都无法仅通过文件 URL 访问 private
文件。相反,你需要创建一个签名 URL 或使用 Drive.get
方法来访问该文件。
// ✅ 有效
const contents = await Drive.get(filePath)
// ❌ 不能通过 URL 访问私有文件
const url = await Drive.getUrl(filePath)
// ✅ 可以使用签名的 url 访问
const signedUrl = await Drive.getSignedUrl(filePath)
写入文件
你可以使用以下方法之一创建/更新文件。如果文件已经存在,它将被更新。
put
put
方法接受文件名作为第一个参数,文件内容(字符串或缓冲区)作为第二个参数。
import Drive from '@ioc:Adonis/Core/Drive'
await Drive.put(filePath, contents)
你还可以使用第三个参数定义文件元数据。
await Drive.put(filePath, contents, {
visibility: 'public',
contentType: 'image/png'
})
以下是可用选项的列表。
选项 | 描述 |
---|---|
visibility |
文件可见性 |
contentType |
文件内容类型 |
contentLanguage |
文件语言。用于设置下载文件时的content-language header |
contentEncoding |
文件内容编码。用于设置下载文件时的content-encoding header |
contentDisposition |
content-disposition 响应标头 |
cacheControl |
cache-control 响应标头的值。 GCS 驱动忽略此选项,因为底层 SDK 不允许配置它 |
putStream
putStream
方法接受内容作为可读流。选项与 put
方法相同。
import Drive from '@ioc:Adonis/Core/Drive'
await Drive.putStream(filePath, readableStream)
主体解析 moveToDisk
你可以使用 file.moveToDisk
方法将用户上传的文件移动到给定磁盘。
该方法接受以下参数。
- 文件位置(没有文件名)。
- 元数据选项。与
put
方法相同。 - (可选)磁盘名称。如果未定义,则使用默认磁盘。
import Drive from '@ioc:Adonis/Core/Drive'
import Route from '@ioc:Adonis/Core/Route'
Route.post('posts', async ({ request }) => {
const coverImage = request.file('cover_image')
// 写入“images”目录
await coverImage.moveToDisk('images')
// 写入“根”目录
await coverImage.moveToDisk('./')
})
moveToDisk
方法将用户上传的文件重命名为唯一/随机文件名。但是,你也可以手动定义文件名。
await coverImage.moveToDisk('images', {
name: `${user.id}.${coverImage.extname}`
})
最后,你还可以定义自定义磁盘名称作为第三个参数。
await coverImage.moveToDisk('images', {}, 's3')
读取文件
你可以使用 Drive.get
或 Drive.getStream
方法读取文件。当文件丢失时,这两种方法都会引发异常。
get
get
方法将文件内容作为缓冲区返回。你可以通过调用 toString
方法将其转换为字符串。
import Drive from '@ioc:Adonis/Core/Drive'
const contents = await Drive.get('filePath')
contents.toString()
// 自定义编码
contents.toString('ascii')
getStream
getStream
方法返回 可读流 的实例。
const readableStream = await Drive.getStream('filePath')
response.stream(readableStream)
生成 URL
你可以使用 Drive.getUrl
或 Drive.getSignedUrl
方法生成文件路径的 URL。
对于云存储提供商,生成的 URL 指向云服务。而对于 local
驱动程序,URL 指向你的 AdonisJS 应用程序。
当 serveAssets
选项在配置文件中设置为 true 时,local
驱动程序会隐式注册路由。此外,basePath
是必需的,并且在已注册的磁盘中必须是唯一的。
{
local: {
driver: 'local',
serveAssets: true,
basePath: '/uploads'
}
}
getUrl
返回下载给定文件的 URL。如果文件丢失,则会引发异常。使用 getUrl
方法返回的 URL 只能查看 public
文件。
const url = await Drive.getUrl('filePath')
getSignedUrl
getSignedUrl
方法返回一个 URL 以下载给定文件及其签名。只要签名有效,你就可以下载文件。
你还可以定义签名过期和 URL 失效的持续时间。
const url = await Drive.getSignedUrl('filePath')
// 过期
const url = await Drive.getSignedUrl('filePath', {
expiresIn: '30mins'
})
最后,你还可以将以下响应内容标头定义为第二个参数。
const url = await Drive.getSignedUrl('filePath', {
contentType: 'application/json',
contentDisposition: 'attachment',
})
以下是可用选项的列表。
选项 | 说明 |
---|---|
contentType |
content-type 响应标头的值 |
contentLanguage |
content-language 响应标头的值。被 GCS 驱动程序忽略 |
contentEncoding |
content-encoding 响应标头的值。被 GCS 驱动程序忽略 |
contentDisposition |
content-disposition 响应标头的值 |
cacheControl |
cache-control 响应标头的值。被 GCS 驱动程序忽略 |
下载文件
下载文件的推荐方法是使用通过 Drive.getUrl
方法生成的文件 URL。但是,你也可以从自定义路径手动下载文件。
以下是流文件的简化示例。 这里有 一个更健壮的实现。
import { extname } from 'path'
import Route from '@ioc:Adonis/Core/Route'
import Drive from '@ioc:Adonis/Core/Drive'
Route.get('/uploads/*', async ({ request, response }) => {
const location = request.param('*').join('/')
const { size } = await Drive.getStats(location)
response.type(extname(location))
response.header('content-length', size)
return response.stream(await Drive.getStream(location))
})
删除文件
你可以使用 Drive.delete
方法删除文件。文件丢失时不会引发异常。
await Drive.delete('avatar.jpg')
复制和移动文件
你可以使用以下方法复制和移动文件。元数据选项与 put
方法相同。
对于云服务,操作在同一个存储桶中执行。因此,例如,如果要从本地磁盘复制文件,则必须使用 put 或 putStream 方法。
await Drive.copy(source, destination, metadataOptions)
await Drive.move(source, destination, metadataOptions)
##在磁盘之间切换
你可以使用 Drive.use
方法在磁盘之间切换。
// 引用 S3 磁盘
const s3 = Drive.use('s3')
await s3.put(filePath, stringOrBuffer)
在运行时切换存储桶
使用 s3
和 gcs
驱动程序时,你可以使用 bucket 方法在运行时切换存储桶。
Drive
.use('s3')
.bucket('bucketName')
.put(filePath, stringOrBuffer)
添加自定义驱动
Drive 公开 API 以添加您的自定义驱动程序。每个驱动程序都必须遵守 DriverContract。
interface DriverContract {
name: string
exists(location: string): Promise<boolean>
get(location: string): Promise<Buffer>
getStream(location: string): Promise<NodeJS.ReadableStream>
getVisibility(location: string): Promise<Visibility>
getStats(location: string): Promise<DriveFileStats>
getSignedUrl(
location: string,
options?: ContentHeaders & { expiresIn?: string | number }
): Promise<string>
getUrl(location: string): Promise<string>
put(
location: string,
contents: Buffer | string,
options?: WriteOptions
): Promise<void>
putStream(
location: string,
contents: NodeJS.ReadableStream,
options?: WriteOptions
): Promise<void>
setVisibility(location: string, visibility: Visibility): Promise<void>
delete(location: string): Promise<void>
copy(
source: string,
destination: string,
options?: WriteOptions
): Promise<void>
move(
source: string,
destination: string,
options?: WriteOptions
): Promise<void>
}
exists
返回一个布尔值以指示文件是否存在。当文件丢失时,驱动程序不应引发异常,而是返回 false。
get
将文件内容作为缓冲区返回。当文件丢失时,驱动程序应该引发异常。
getStream
将文件内容作为可读流返回。当文件丢失时,驱动程序应该引发异常。
getVisibility
返回文件可见性。如果驱动程序不支持文件级可见性,它应该从配置中返回磁盘可见性。
getStats
返回文件元数据。响应对象必须包含以下属性。
{
size: number
modified: Date
isFile: boolean
etag?: string // Optional
}
getSignedUrl
返回签名的 URL 以下载文件。如果可能,签名 URL 在生成 URL 时可以接受响应内容标头。
getUrl
返回文件的静态 URL。无需检查文件是否存在。相反,在提供文件时返回 404。
put
从原始内容(字符串或缓冲区)创建/更新文件。你还必须创建所需的目录。
putStream
从可读流创建/更新文件。你还必须创建所需的目录。
setVisibility
更新文件可见性。如果驱动程序不支持文件级别的可见性,那么它应该忽略该请求。
delete
删除文件。当文件丢失时,驱动程序不应引发异常。
copy
将文件从一个位置复制到另一个位置。复制操作也应该复制文件的元数据。例如:在 S3 中,它需要一个额外的请求来复制文件 ACL。
move
将文件从一个位置移动到另一个位置。移动操作也应该复制文件的元数据。
由外向内扩展
任何时候你都在扩展框架的核心。最好假设你无权访问应用程序代码及其依赖项。换句话说,像编写第三方包一样编写扩展,并使用依赖注入来依赖其他依赖。
出于演示目的,让我们创建一个没有实现的虚拟驱动程序。
mkdir providers/DummyDriver
touch providers/DummyDriver/index.ts
打开 DummyDriver/index.ts
文件并将以下内容粘贴到其中。
import type {
Visibility,
WriteOptions,
ContentHeaders,
DriveFileStats,
DriverContract,
} from '@ioc:Adonis/Core/Drive'
export interface DummyDriverContract extends DriverContract {
name: 'dummy' // 驱动程序名称
}
export type DummyDriverConfig = {
driver: 'dummy' // 驱动程序名称
// .. 其他配置选项
}
export class DummyDriver implements DummyDriverContract {
// 实现在这里
}
接下来,你必须将驱动程序注册到 Drive 模块。你必须在服务提供者的引导方法中执行此操作。打开预先存在的 providers/AppProvider.ts
文件并将以下代码粘贴到其中。
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
constructor(protected app: ApplicationContract) {}
public async boot() {
const { DummyDriver } = await import('./DummyDriver')
const Drive = this.app.container.use('Adonis/Core/Drive')
Drive.extend('dummy', (_drive, _diskName, config) => {
return new DummyDriver(config)
})
}
}
通知 TypeScript 新驱动程序
在有人可以在 config/drive.ts
文件中引用此驱动程序之前。你必须告知 TypeScript 静态编译器它的存在。
如果你正在创建一个包,那么你可以在你的包主文件中编写以下代码,否则你可以将它写在 contracts/drive.ts
文件中。
import {
DummyDriverConfig,
DummyDriverContract
} from '../providers/DummyDriver'
declare module '@ioc:Adonis/Core/Drive' {
interface DriversList {
dummy: {
config: DummyDriverConfig,
implementation: DummyDriverContract
}
}
}
使用驱动程序
好的,我们现在可以使用驱动程序了。让我们首先在 config/drive.ts
文件中定义新磁盘的配置。
{
disks: {
myDummyDisk: {
driver: 'dummy',
// ... 其余的配置
}
}
}
并按如下方式使用。
import Drive from '@ioc:Adonis/Core/Drive'
await Drive.use('myDummyDisk').put(filePath, contents)
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。