Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
@@ -0,0 +1,132 @@
import type { Entry } from '#base_modules/FileManager/types/entries'
export type EntryFilter = 'media' | 'archives' | 'documents' | 'all'
export type EntrySortBy = 'name' | 'size' | 'type' | 'date'
export type EntrySortOrder = 'asc' | 'desc'
export interface EntryListParams {
filterBy?: EntryFilter
sortBy?: EntrySortBy
sortOrder?: EntrySortOrder
maxResults?: number
paginationToken?: string
}
export interface EntryListResult {
data: Entry[]
meta: {
nextPaginationToken: string | null
}
}
interface PaginationTokenPayload {
offset: number
}
const FILTER_EXTENSIONS: Record<Exclude<EntryFilter, 'all'>, string[]> = {
media: ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mp3', 'wav'],
archives: ['zip', 'rar', '7z'],
documents: ['pdf', 'doc', 'docx', 'txt'],
}
export default class EntryListService {
public apply(entries: Entry[], params: EntryListParams = {}): EntryListResult {
const {
filterBy = 'all',
sortBy = 'name',
sortOrder = 'asc',
maxResults = 25,
paginationToken,
} = params
const limit = Math.min(Math.max(maxResults, 1), 100)
const decoded = this.decodeToken(paginationToken)
const offset = decoded?.offset ?? 0
const result = this.sortEntries(this.filterEntries(entries, filterBy), sortBy, sortOrder)
const data = result.slice(offset, offset + limit)
const nextOffset = offset + limit
const nextPaginationToken =
nextOffset < result.length
? this.encodeToken({
offset: nextOffset,
sortBy,
sortOrder,
filterBy,
})
: null
return {
data,
meta: {
nextPaginationToken,
},
}
}
private filterEntries(entries: Entry[], filterBy: EntryFilter): Entry[] {
if (filterBy === 'all') return entries
return entries.filter((entry) => {
if (entry.type === 'folder') return true
const ext = entry.name.split('.').pop()?.toLowerCase()
if (!ext) return false
return FILTER_EXTENSIONS[filterBy].includes(ext)
})
}
private sortEntries(entries: Entry[], sortBy: EntrySortBy, sortOrder: EntrySortOrder): Entry[] {
return [...entries].sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1
}
let valA: string | number
let valB: string | number
switch (sortBy) {
case 'size':
valA = a.type === 'file' ? (a.size ?? 0) : 0
valB = b.type === 'file' ? (b.size ?? 0) : 0
break
case 'date':
valA = a.type === 'file' ? new Date(a.updatedAt ?? 0).getTime() : 0
valB = b.type === 'file' ? new Date(b.updatedAt ?? 0).getTime() : 0
break
case 'type':
valA = a.name.split('.').pop() ?? ''
valB = b.name.split('.').pop() ?? ''
break
case 'name':
default:
valA = a.name.toLowerCase()
valB = b.name.toLowerCase()
}
if (valA < valB) return sortOrder === 'desc' ? 1 : -1
if (valA > valB) return sortOrder === 'desc' ? -1 : 1
return 0
})
}
private encodeToken(payload: object): string {
return Buffer.from(JSON.stringify(payload)).toString('base64')
}
private decodeToken(token?: string): PaginationTokenPayload | null {
if (!token) return null
try {
return JSON.parse(Buffer.from(token, 'base64').toString('utf-8'))
} catch {
return null
}
}
}
@@ -0,0 +1,60 @@
import type { Entry, FolderEntry } from '#base_modules/FileManager/types/entries'
export default class EntrySearchService {
public search(entries: Entry[], query: string, signal?: AbortSignal): Entry[] {
if (signal?.aborted) return []
const results: Entry[] = []
const addedFolders = new Set<string>()
const q = query.toLowerCase()
for (const entry of entries) {
if (signal?.aborted) return results
if (entry.type === 'folder') {
this.addFolderIfMatches(entry, q, addedFolders, results)
continue
}
if (entry.path.toLowerCase().includes(q)) {
results.push(entry)
}
for (const folder of this.getFileParentFolders(entry.path)) {
this.addFolderIfMatches(folder, q, addedFolders, results)
}
}
return results
}
private addFolderIfMatches(
folder: FolderEntry,
query: string,
addedFolders: Set<string>,
results: Entry[]
) {
if (!folder.path.toLowerCase().includes(query) || addedFolders.has(folder.path)) return
addedFolders.add(folder.path)
results.push(folder)
}
private getFileParentFolders(path: string): FolderEntry[] {
const segments = path.replace(/^\/+/, '').split('/').slice(0, -1)
const folders: FolderEntry[] = []
let current = ''
for (const segment of segments) {
current = current ? `${current}/${segment}` : segment
folders.push({
name: segment,
path: current,
type: 'folder',
})
}
return folders
}
}
@@ -0,0 +1,131 @@
import type { MultipartFile } from '@adonisjs/core/bodyparser'
import type { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import {
default as EntryListService,
type EntryListParams,
type EntryListResult,
} from '#base_modules/FileManager/services/entry_list_service'
import createStorageService from '#base_modules/FileManager/services/storage_service/create_storage_service'
import type {
FileStream,
StorageService,
} from '#base_modules/FileManager/services/storage_service/storage_service_interface'
import FilePreviewService from '#base_modules/FileManager/services/file_preview_service'
import EntrySearchService from '#base_modules/FileManager/services/entry_search_service'
import PathService from '#base_modules/FileManager/services/path_service'
import {
EntryExistsException,
FilenameMissingExtensionException,
} from '#exceptions/file_manager_exceptions'
export default class FileManagerService {
private filePreviewService = new FilePreviewService()
private entrySearchService = new EntrySearchService()
private entryListService = new EntryListService()
private pathService = new PathService()
private storageService: StorageService
constructor(storageService?: StorageService) {
if (storageService) this.storageService = storageService
else this.storageService = createStorageService()
}
public async listDirectory(path: string, params: EntryListParams): Promise<EntryListResult> {
const entries = await this.storageService.listDirectoryEntries(path)
return this.entryListService.apply(entries, params)
}
public async getFileEntryWithPreview(path: string): Promise<FileEntry & { content?: string }> {
const entry = await this.storageService.getFileEntry(path)
return this.filePreviewService.withPreview(entry, () =>
this.storageService.getFileContent(path)
)
}
public getFileStream(path: string): Promise<FileStream> {
return this.storageService.getFileStream(path)
}
public async createDirectory(path: string, name: string): Promise<FolderEntry> {
const result = await this.storageService.createDirectory(path, name)
return {
name,
path: result.path,
type: 'folder',
}
}
public async deleteEntry(path: string): Promise<void> {
await this.storageService.deleteEntry(path)
}
public async moveEntry(path: string, name: string): Promise<Entry> {
const entry = await this.storageService.getEntry(path)
const dir = this.pathService.dirname(path)
const newPath = dir ? this.pathService.join(dir, name) : name
if (entry.type === 'file' && !this.pathService.getFileNameMeta(name).ext) {
throw new FilenameMissingExtensionException()
}
if (await this.storageService.entryExists(newPath)) {
throw new EntryExistsException()
}
const result = await this.storageService.moveEntry(path, newPath)
return this.storageService.getEntry(result.path)
}
public async writeFile(path: string, file: MultipartFile): Promise<Entry> {
const dir = this.pathService.toStoragePath(path)
const safeName = await this.pathService.generateUniqueName(dir, file.clientName, (target) => {
return this.storageService.entryExists(target)
})
const writePath = this.pathService.join(dir, safeName)
const result = await this.storageService.writeFile(writePath, file)
return this.storageService.getEntry(result.path)
}
public async copyEntries(paths: string[]): Promise<Entry[]> {
const results: Entry[] = []
for (const path of paths) {
try {
const sourceEntry = await this.storageService.getFileEntry(path)
const dir = this.pathService.dirname(sourceEntry.path)
const newName = await this.pathService.generateUniqueName(
dir,
sourceEntry.name,
(target) => this.storageService.entryExists(target),
(data) => {
const template = 'copy_'
const regex = new RegExp(`\\s?${template}(\\d+)$`)
const base = data.base.replace(regex, '').trim()
return `${base} ${template}${data.count}${data.extension}`
}
)
const copyPath = this.pathService.join(dir, newName)
const result = await this.storageService.copyEntry(path, copyPath)
const copiedEntry = await this.storageService.getFileEntry(result.path)
results.push(copiedEntry)
} catch {
continue
}
}
return results
}
public async search(path: string, query: string, signal?: AbortSignal): Promise<Entry[]> {
const entries = await this.storageService.listAllEntries(path)
return this.entrySearchService.search(entries, query, signal)
}
}
@@ -0,0 +1,22 @@
import type { FileEntry } from '#base_modules/FileManager/types/entries'
const PREVIEWABLE_TEXT_EXTENSIONS = ['txt', 'md', 'log', 'json', 'csv']
export default class FilePreviewService {
public async withPreview(
entry: FileEntry,
getContent: () => Promise<string>
): Promise<FileEntry & { content?: string }> {
if (!this.isPreviewableTextFile(entry.name)) return entry
const content = await getContent()
return { ...entry, content }
}
private isPreviewableTextFile(name: string): boolean {
const fileExt = name.split('.').pop()?.toLowerCase()
return PREVIEWABLE_TEXT_EXTENSIONS.includes(fileExt as string)
}
}
@@ -0,0 +1,84 @@
import { randomUUID } from 'node:crypto'
import { posix } from 'node:path'
export interface FileNameMeta {
name: string
ext: string
}
export interface UniqueNameFormatterParams {
base: string
count: number
extension: string
}
export type UniqueNameFormatter = (params: UniqueNameFormatterParams) => string
export default class PathService {
public toStoragePath(path: string = ''): string {
let safePath = path.replace(/^(\.\.(\/|\\|$))+/, '')
safePath = safePath.startsWith('/') ? safePath.slice(1) : safePath
return safePath
}
public toClientPath(path: string = ''): string {
return path.replace(/^\/?uploads\//, '/')
}
public join(...parts: string[]): string {
return posix.join(...parts)
}
public dirname(path: string): string {
return this.toStoragePath(path).split('/').slice(0, -1).join('/')
}
public basename(path: string): string {
return path.split('/').reverse()[0]
}
public getFileNameMeta(name: string): FileNameMeta {
const extIndex = name.lastIndexOf('.')
const rawName = extIndex !== -1 ? name.slice(0, extIndex) : name
const ext = extIndex !== -1 ? name.slice(extIndex).trim() : ''
return {
name: rawName,
ext,
}
}
public sanitizeFileName(name: string): string {
return name.replace(/[()]/g, '').replace(/[^a-zA-Z0-9.\-_ ]/g, '')
}
public async generateUniqueName(
dir: string,
fileName: string,
exists: (path: string) => Promise<boolean>,
formatter?: UniqueNameFormatter
): Promise<string> {
const { ext, name } = this.getFileNameMeta(fileName)
const safeBase = this.sanitizeFileName(name)
let counter = 1
let newName = `${safeBase}${ext}`
while (await exists(this.join(dir, newName))) {
newName =
typeof formatter === 'function'
? formatter({ base: safeBase, count: counter, extension: ext })
: `${safeBase}-${counter}${ext}`
counter++
if (counter > 5) {
newName = `${randomUUID()}${ext}`
break
}
}
return newName
}
}
@@ -0,0 +1,21 @@
import env from '#start/env'
import type { 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)
}
}
@@ -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,274 @@
import drive from '@adonisjs/drive/services/main'
import {
DirExistsException,
DirNotFoundException,
FileNotFoundException,
} from '#exceptions/file_manager_exceptions'
import PathService from '#base_modules/FileManager/services/path_service'
import type { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import type { StorageService } from '#base_modules/FileManager/services/storage_service/storage_service_interface'
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 directoryPrefix(path: string): string {
return path ? `${path.replace(/\/$/, '')}/` : path
}
private async listDirectoryFiles(path: string): Promise<{ key: string; relative: string }[]> {
const prefix = this.directoryPrefix(path)
const files: { key: string; relative: string }[] = []
let paginationToken: string | undefined
do {
const response = await this.disk.listAll(prefix, { recursive: true, paginationToken })
for (const item of response.objects) {
if (!item.isFile || !item.key.startsWith(prefix)) continue
files.push({ key: item.key, relative: item.key.slice(prefix.length) })
}
paginationToken = response.paginationToken
} while (paginationToken)
return files
}
private async moveDirectory(oldPath: string, newPath: string) {
await this.disk.put(this.directoryMarkerService.markerPath(newPath), '')
for (const file of await this.listDirectoryFiles(oldPath)) {
const target = this.pathService.join(newPath, file.relative)
const dir = this.pathService.dirname(target)
if (dir) await this.disk.put(this.directoryMarkerService.markerPath(dir), '')
await this.disk.move(file.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> {
if (await this.disk.exists(this.directoryMarkerService.markerPath(path))) return true
try {
const response = await this.disk.listAll(this.directoryPrefix(path))
return Array.from(response.objects).length > 0
} catch {
return false
}
}
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)
return this.getDirectoryEntry(path)
}
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,291 @@
import { createReadStream } from 'node:fs'
import drive from '@adonisjs/drive/services/main'
import {
DirExistsException,
DirNotFoundException,
FileNotFoundException,
} from '#exceptions/file_manager_exceptions'
import PathService from '#base_modules/FileManager/services/path_service'
import type { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import type { StorageService } from '#base_modules/FileManager/services/storage_service/storage_service_interface'
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 listDirectoryFiles(path: string): Promise<{ key: string; relative: string }[]> {
const prefix = this.directoryPrefix(path)
const files: { key: string; relative: string }[] = []
let paginationToken: string | undefined
do {
const response = await this.disk.listAll(prefix, { recursive: true, paginationToken })
for (const item of response.objects) {
if (!item.isFile || !item.key.startsWith(prefix)) continue
files.push({ key: item.key, relative: item.key.slice(prefix.length) })
}
paginationToken = response.paginationToken
} while (paginationToken)
return files
}
private async moveDirectory(oldPath: string, newPath: string) {
await this.disk.put(this.directoryMarkerService.markerPath(newPath), '')
for (const file of await this.listDirectoryFiles(oldPath)) {
const target = this.pathService.join(newPath, file.relative)
const dir = this.pathService.dirname(target)
if (dir) await this.disk.put(this.directoryMarkerService.markerPath(dir), '')
await this.disk.move(file.key, target)
}
}
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> {
if (await this.disk.exists(this.directoryMarkerService.markerPath(path))) return true
try {
const response = await this.disk.listAll(this.directoryPrefix(path))
return Array.from(response.objects).length > 0
} catch {
return false
}
}
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 {
for (const file of await this.listDirectoryFiles(storagePath)) {
await this.disk.delete(file.key)
}
}
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)
return this.getDirectoryEntry(path)
}
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) }
}
}
@@ -0,0 +1,25 @@
import type { Readable } from 'node:stream'
import type { MultipartFile } from '@adonisjs/core/bodyparser'
import type { 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 }>
}