Initial commit
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
import env from '#start/env'
|
||||
import { StorageService } from './storage_service_interface.js'
|
||||
import { FSStorageService } from './fs_storage_service.js'
|
||||
import { S3StorageService } from './s3_storage_service.js'
|
||||
|
||||
type StorageDisk = 'fs' | 's3'
|
||||
|
||||
export default function createStorageService(
|
||||
disk: StorageDisk = env.get('DRIVE_DISK')
|
||||
): StorageService {
|
||||
switch (disk) {
|
||||
case 'fs':
|
||||
return new FSStorageService()
|
||||
|
||||
case 's3':
|
||||
return new S3StorageService()
|
||||
|
||||
default:
|
||||
throw new Error('Unsupported: ' + disk)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export default class FSDirectoryMarkerService {
|
||||
public markerName = '.keep'
|
||||
|
||||
public markerPath(path: string): string {
|
||||
return `${path}/${this.markerName}`
|
||||
}
|
||||
|
||||
public isMarkerName(name: string): boolean {
|
||||
return name === this.markerName
|
||||
}
|
||||
|
||||
public isMarkerPath(path: string): boolean {
|
||||
return path.endsWith(`/${this.markerName}`)
|
||||
}
|
||||
|
||||
public directoryPathFromMarker(path: string): string {
|
||||
return path.replace(new RegExp(`/${this.markerName}$`), '')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import drive from '@adonisjs/drive/services/main'
|
||||
import {
|
||||
DirExistsException,
|
||||
DirNotFoundException,
|
||||
FileNotFoundException,
|
||||
} from '#exceptions/file_manager_exeptions'
|
||||
import { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
|
||||
import { StorageService } from './storage_service_interface.js'
|
||||
import PathService from '#base_modules/FileManager/services/path_service'
|
||||
import FSDirectoryMarkerService from '#base_modules/FileManager/services/storage_service/fs_directory_marker_service'
|
||||
|
||||
export class FSStorageService implements StorageService {
|
||||
private disk = drive.use('fs')
|
||||
private pathService = new PathService()
|
||||
private directoryMarkerService = new FSDirectoryMarkerService()
|
||||
|
||||
private async moveDirectory(oldPath: string, newPath: string) {
|
||||
const response = await this.disk.listAll(oldPath, { recursive: true })
|
||||
|
||||
for (const item of response.objects) {
|
||||
if (!item.isFile) continue
|
||||
|
||||
const relative = item.key.slice(oldPath.length).replace(/^\/+/, '')
|
||||
|
||||
const target = `${newPath}/${relative}`
|
||||
|
||||
const dir = target.split('/').slice(0, -1).join('/')
|
||||
|
||||
if (dir) await this.disk.put(this.directoryMarkerService.markerPath(dir), '')
|
||||
|
||||
await this.disk.move(item.key, target)
|
||||
}
|
||||
|
||||
await this.disk.deleteAll(oldPath)
|
||||
}
|
||||
|
||||
public entryExists: StorageService['entryExists'] = async (path) => {
|
||||
const storagePath = this.pathService.toStoragePath(path)
|
||||
|
||||
if (await this.disk.exists(storagePath)) return true
|
||||
|
||||
return this.directoryExists(storagePath)
|
||||
}
|
||||
|
||||
private async directoryExists(path: string): Promise<boolean> {
|
||||
return this.disk.exists(this.directoryMarkerService.markerPath(path))
|
||||
}
|
||||
|
||||
private async getFileEntryFromStoragePath(
|
||||
storagePath: string,
|
||||
params: {
|
||||
name?: string
|
||||
path?: string
|
||||
url?: string
|
||||
} = {}
|
||||
): Promise<FileEntry> {
|
||||
const metadata = await this.disk.getMetaData(storagePath)
|
||||
const path = params.path ?? this.pathService.toClientPath(storagePath)
|
||||
const url = params.url ?? (await this.disk.getUrl(storagePath))
|
||||
|
||||
return {
|
||||
name: params.name ?? this.pathService.basename(path),
|
||||
type: 'file',
|
||||
size: metadata?.contentLength ?? 0,
|
||||
path,
|
||||
url,
|
||||
updatedAt: metadata.lastModified.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
public listDirectoryEntries: StorageService['listDirectoryEntries'] = async (path) => {
|
||||
const diskPath = this.pathService.toStoragePath(path)
|
||||
|
||||
const response = await this.disk.listAll(diskPath)
|
||||
|
||||
const entries = await Promise.all(
|
||||
[...response.objects].map(async (file) => {
|
||||
if (file.isFile && !this.directoryMarkerService.isMarkerName(file.name)) {
|
||||
const relativePath = this.pathService.toClientPath(file.key)
|
||||
|
||||
return this.getFileEntryFromStoragePath(file.key, {
|
||||
name: file.name,
|
||||
path: relativePath,
|
||||
url: await this.disk.getUrl(relativePath),
|
||||
})
|
||||
}
|
||||
|
||||
if (file.isDirectory) {
|
||||
return {
|
||||
name: file.name,
|
||||
type: 'folder',
|
||||
path: this.pathService.toClientPath(file.prefix),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
)
|
||||
|
||||
return entries.filter(Boolean) as Entry[]
|
||||
}
|
||||
|
||||
public listAllEntries: StorageService['listAllEntries'] = async (path) => {
|
||||
let response: Awaited<ReturnType<typeof this.disk.listAll>>
|
||||
|
||||
try {
|
||||
response = await this.disk.listAll(this.pathService.toStoragePath(path), { recursive: true })
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const entries = await Promise.all(
|
||||
[...response.objects].map(async (item) => {
|
||||
if (item.isDirectory) return null
|
||||
|
||||
if (this.directoryMarkerService.isMarkerName(item.name)) {
|
||||
const folderPath = this.directoryMarkerService.directoryPathFromMarker(item.key)
|
||||
|
||||
return {
|
||||
name: this.pathService.basename(folderPath) || 'root',
|
||||
path: this.pathService.toClientPath(folderPath),
|
||||
type: 'folder',
|
||||
}
|
||||
}
|
||||
|
||||
return this.getFileEntryFromStoragePath(item.key, {
|
||||
name: item.name,
|
||||
path: this.pathService.toClientPath(item.key),
|
||||
url: '',
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return entries.filter(Boolean) as Entry[]
|
||||
}
|
||||
|
||||
public createDirectory: StorageService['createDirectory'] = async (path, name) => {
|
||||
const dirPath = this.pathService.join(this.pathService.toStoragePath(path), name)
|
||||
|
||||
const isExists = await this.directoryExists(dirPath)
|
||||
|
||||
if (isExists) throw new DirExistsException()
|
||||
|
||||
await this.disk.put(this.directoryMarkerService.markerPath(dirPath), '')
|
||||
|
||||
return { path: this.pathService.toClientPath(dirPath) }
|
||||
}
|
||||
|
||||
public deleteEntry: StorageService['deleteEntry'] = async (path) => {
|
||||
await this.disk.deleteAll(this.pathService.toStoragePath(path))
|
||||
return { path }
|
||||
}
|
||||
|
||||
public moveEntry: StorageService['moveEntry'] = async (path, newPath) => {
|
||||
const oldPath = this.pathService.toStoragePath(path)
|
||||
const targetPath = this.pathService.toStoragePath(newPath)
|
||||
const entry = await this.getEntry(path)
|
||||
|
||||
if (entry.type === 'file') {
|
||||
await this.disk.move(oldPath, targetPath)
|
||||
} else {
|
||||
await this.moveDirectory(oldPath, targetPath)
|
||||
}
|
||||
|
||||
return { path: this.pathService.toClientPath(targetPath) }
|
||||
}
|
||||
|
||||
public getFileEntry: StorageService['getFileEntry'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
return this.getFileEntryFromStoragePath(source, {
|
||||
name: this.pathService.basename(path),
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
public getFileContent: StorageService['getFileContent'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
return this.disk.get(source)
|
||||
}
|
||||
|
||||
public getFileStream: StorageService['getFileStream'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
const metadata = await this.disk.getMetaData(source)
|
||||
const stream = await this.disk.getStream(source)
|
||||
const name = this.pathService.basename(path)
|
||||
|
||||
return {
|
||||
stream,
|
||||
contentType: metadata.contentType,
|
||||
size: metadata.contentLength,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
private async getDirectoryEntry(path: string): Promise<FolderEntry> {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const isExists = await this.directoryExists(source)
|
||||
|
||||
if (!isExists) throw new DirNotFoundException()
|
||||
|
||||
const name = this.pathService.basename(path) || 'root'
|
||||
|
||||
return { type: 'folder', name, path }
|
||||
}
|
||||
|
||||
public getEntry: StorageService['getEntry'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (exists) return this.getFileEntry(path)
|
||||
|
||||
const response = await this.disk.listAll(source)
|
||||
|
||||
if (Array.from(response.objects).length) return this.getDirectoryEntry(path)
|
||||
|
||||
throw new Error('Entry not found')
|
||||
}
|
||||
|
||||
public writeFile: StorageService['writeFile'] = async (path, file) => {
|
||||
const writePath = this.pathService.toStoragePath(path)
|
||||
await file.moveToDisk(writePath)
|
||||
|
||||
return { path: this.pathService.toClientPath(writePath) }
|
||||
}
|
||||
|
||||
public copyEntry: StorageService['copyEntry'] = async (sourcePath, targetPath) => {
|
||||
const source = this.pathService.toStoragePath(sourcePath)
|
||||
const target = this.pathService.toStoragePath(targetPath)
|
||||
await this.disk.copy(source, target)
|
||||
|
||||
return { path: this.pathService.toClientPath(target) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { createReadStream } from 'node:fs'
|
||||
import drive from '@adonisjs/drive/services/main'
|
||||
import {
|
||||
DirExistsException,
|
||||
DirNotFoundException,
|
||||
FileNotFoundException,
|
||||
} from '#exceptions/file_manager_exeptions'
|
||||
import { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
|
||||
import { StorageService } from './storage_service_interface.js'
|
||||
import PathService from '#base_modules/FileManager/services/path_service'
|
||||
import FSDirectoryMarkerService from '#base_modules/FileManager/services/storage_service/fs_directory_marker_service'
|
||||
|
||||
export class S3StorageService implements StorageService {
|
||||
private disk = drive.use('s3')
|
||||
private pathService = new PathService()
|
||||
private directoryMarkerService = new FSDirectoryMarkerService()
|
||||
|
||||
private directoryPrefix(path: string): string {
|
||||
return path ? `${path.replace(/\/$/, '')}/` : path
|
||||
}
|
||||
|
||||
private async moveDirectory(oldPath: string, newPath: string) {
|
||||
const oldPrefix = this.directoryPrefix(oldPath)
|
||||
const response = await this.disk.listAll(oldPrefix, { recursive: true })
|
||||
|
||||
for (const item of response.objects) {
|
||||
if (!item.isFile) continue
|
||||
|
||||
const relative = item.key.slice(oldPrefix.length).replace(/^\/+/, '')
|
||||
const target = this.pathService.join(newPath, relative)
|
||||
const dir = target.split('/').slice(0, -1).join('/')
|
||||
|
||||
if (dir) await this.disk.put(this.directoryMarkerService.markerPath(dir), '')
|
||||
|
||||
await this.disk.move(item.key, target)
|
||||
}
|
||||
|
||||
await this.disk.deleteAll(oldPrefix)
|
||||
}
|
||||
|
||||
public entryExists: StorageService['entryExists'] = async (path) => {
|
||||
const storagePath = this.pathService.toStoragePath(path)
|
||||
|
||||
if (await this.disk.exists(storagePath)) return true
|
||||
|
||||
return this.directoryExists(storagePath)
|
||||
}
|
||||
|
||||
private async directoryExists(path: string): Promise<boolean> {
|
||||
return this.disk.exists(this.directoryMarkerService.markerPath(path))
|
||||
}
|
||||
|
||||
private async getFileEntryFromStoragePath(
|
||||
storagePath: string,
|
||||
params: {
|
||||
name?: string
|
||||
path?: string
|
||||
url?: string
|
||||
} = {}
|
||||
): Promise<FileEntry> {
|
||||
const metadata = await this.disk.getMetaData(storagePath)
|
||||
const path = params.path ?? this.pathService.toClientPath(storagePath)
|
||||
const url = params.url ?? (await this.disk.getUrl(storagePath))
|
||||
|
||||
return {
|
||||
name: params.name ?? this.pathService.basename(path),
|
||||
type: 'file',
|
||||
size: metadata?.contentLength ?? 0,
|
||||
path,
|
||||
url,
|
||||
updatedAt: metadata.lastModified.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
public listDirectoryEntries: StorageService['listDirectoryEntries'] = async (path) => {
|
||||
const diskPath = this.pathService.toStoragePath(path)
|
||||
const response = await this.disk.listAll(diskPath)
|
||||
|
||||
const entries = await Promise.all(
|
||||
[...response.objects].map(async (file) => {
|
||||
if (file.isFile && !this.directoryMarkerService.isMarkerName(file.name)) {
|
||||
const relativePath = this.pathService.toClientPath(file.key)
|
||||
|
||||
return this.getFileEntryFromStoragePath(file.key, {
|
||||
name: file.name,
|
||||
path: relativePath,
|
||||
url: await this.disk.getUrl(relativePath),
|
||||
})
|
||||
}
|
||||
|
||||
if (file.isDirectory) {
|
||||
return {
|
||||
name: file.name,
|
||||
type: 'folder',
|
||||
path: this.pathService.toClientPath(file.prefix),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
)
|
||||
|
||||
return entries.filter(Boolean) as Entry[]
|
||||
}
|
||||
|
||||
public listAllEntries: StorageService['listAllEntries'] = async (path) => {
|
||||
let response: Awaited<ReturnType<typeof this.disk.listAll>>
|
||||
|
||||
try {
|
||||
const storagePath = this.pathService.toStoragePath(path)
|
||||
response = await this.disk.listAll(this.directoryPrefix(storagePath), { recursive: true })
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const entries = await Promise.all(
|
||||
[...response.objects].map(async (item) => {
|
||||
if (item.isDirectory) return null
|
||||
|
||||
if (this.directoryMarkerService.isMarkerName(item.name)) {
|
||||
const folderPath = this.directoryMarkerService.directoryPathFromMarker(item.key)
|
||||
|
||||
return {
|
||||
name: this.pathService.basename(folderPath) || 'root',
|
||||
path: this.pathService.toClientPath(folderPath),
|
||||
type: 'folder',
|
||||
}
|
||||
}
|
||||
|
||||
return this.getFileEntryFromStoragePath(item.key, {
|
||||
name: item.name,
|
||||
path: this.pathService.toClientPath(item.key),
|
||||
url: '',
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return entries.filter(Boolean) as Entry[]
|
||||
}
|
||||
|
||||
public createDirectory: StorageService['createDirectory'] = async (path, name) => {
|
||||
const dirPath = this.pathService.join(this.pathService.toStoragePath(path), name)
|
||||
|
||||
const isExists = await this.directoryExists(dirPath)
|
||||
|
||||
if (isExists) throw new DirExistsException()
|
||||
|
||||
await this.disk.put(this.directoryMarkerService.markerPath(dirPath), '')
|
||||
|
||||
return { path: this.pathService.toClientPath(dirPath) }
|
||||
}
|
||||
|
||||
public deleteEntry: StorageService['deleteEntry'] = async (path) => {
|
||||
const storagePath = this.pathService.toStoragePath(path)
|
||||
const entry = await this.getEntry(path)
|
||||
|
||||
if (entry.type === 'file') {
|
||||
await this.disk.delete(storagePath)
|
||||
} else {
|
||||
await this.disk.deleteAll(this.directoryPrefix(storagePath))
|
||||
}
|
||||
|
||||
return { path }
|
||||
}
|
||||
|
||||
public moveEntry: StorageService['moveEntry'] = async (path, newPath) => {
|
||||
const oldPath = this.pathService.toStoragePath(path)
|
||||
const targetPath = this.pathService.toStoragePath(newPath)
|
||||
const entry = await this.getEntry(path)
|
||||
|
||||
if (entry.type === 'file') {
|
||||
await this.disk.move(oldPath, targetPath)
|
||||
} else {
|
||||
await this.moveDirectory(oldPath, targetPath)
|
||||
}
|
||||
|
||||
return { path: this.pathService.toClientPath(targetPath) }
|
||||
}
|
||||
|
||||
public getFileEntry: StorageService['getFileEntry'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
return this.getFileEntryFromStoragePath(source, {
|
||||
name: this.pathService.basename(path),
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
public getFileContent: StorageService['getFileContent'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
return this.disk.get(source)
|
||||
}
|
||||
|
||||
public getFileStream: StorageService['getFileStream'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (!exists) throw new FileNotFoundException()
|
||||
|
||||
const metadata = await this.disk.getMetaData(source)
|
||||
const stream = await this.disk.getStream(source)
|
||||
const name = this.pathService.basename(path)
|
||||
|
||||
return {
|
||||
stream,
|
||||
contentType: metadata.contentType,
|
||||
size: metadata.contentLength,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
private async getDirectoryEntry(path: string): Promise<FolderEntry> {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const isExists = await this.directoryExists(source)
|
||||
|
||||
if (!isExists) throw new DirNotFoundException()
|
||||
|
||||
const name = this.pathService.basename(path) || 'root'
|
||||
|
||||
return { type: 'folder', name, path }
|
||||
}
|
||||
|
||||
public getEntry: StorageService['getEntry'] = async (path) => {
|
||||
const source = this.pathService.toStoragePath(path)
|
||||
|
||||
const exists = await this.disk.exists(source)
|
||||
|
||||
if (exists) return this.getFileEntry(path)
|
||||
|
||||
const response = await this.disk.listAll(source)
|
||||
|
||||
if (Array.from(response.objects).length) return this.getDirectoryEntry(path)
|
||||
|
||||
throw new Error('Entry not found')
|
||||
}
|
||||
|
||||
public writeFile: StorageService['writeFile'] = async (path, file) => {
|
||||
const writePath = this.pathService.toStoragePath(path)
|
||||
|
||||
if (!file.tmpPath) {
|
||||
throw new Error('Cannot write multipart file to S3 without a temporary file path')
|
||||
}
|
||||
|
||||
await this.disk.putStream(writePath, createReadStream(file.tmpPath), {
|
||||
contentType: file.headers['content-type'],
|
||||
contentLength: file.size,
|
||||
})
|
||||
|
||||
return { path: this.pathService.toClientPath(writePath) }
|
||||
}
|
||||
|
||||
public copyEntry: StorageService['copyEntry'] = async (sourcePath, targetPath) => {
|
||||
const source = this.pathService.toStoragePath(sourcePath)
|
||||
const target = this.pathService.toStoragePath(targetPath)
|
||||
await this.disk.copy(source, target)
|
||||
|
||||
return { path: this.pathService.toClientPath(target) }
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Readable } from 'node:stream'
|
||||
import { MultipartFile } from '@adonisjs/core/bodyparser'
|
||||
import { Entry, FileEntry } from '#base_modules/FileManager/types/entries'
|
||||
|
||||
export interface FileStream {
|
||||
stream: Readable
|
||||
contentType?: string
|
||||
size?: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface StorageService {
|
||||
listDirectoryEntries(path: string): Promise<Entry[]>
|
||||
listAllEntries(path: string): Promise<Entry[]>
|
||||
entryExists(path: string): Promise<boolean>
|
||||
getEntry(path: string): Promise<Entry>
|
||||
getFileEntry(path: string): Promise<FileEntry>
|
||||
getFileContent(path: string): Promise<string>
|
||||
getFileStream(path: string): Promise<FileStream>
|
||||
writeFile(path: string, file: MultipartFile): Promise<{ path: string }>
|
||||
createDirectory(path: string, name: string): Promise<{ path: string }>
|
||||
moveEntry(oldPath: string, newPath: string): Promise<{ path: string }>
|
||||
deleteEntry(path: string): Promise<{ path: string }>
|
||||
copyEntry(sourcePath: string, targetPath: string): Promise<{ path: string }>
|
||||
}
|
||||
Reference in New Issue
Block a user