Initial commit

This commit is contained in:
Igor Martyniuk
2026-06-25 15:53:49 +03:00
commit 1bb0b7e68a
1517 changed files with 218469 additions and 0 deletions
@@ -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)
}
}
@@ -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) }
}
}
@@ -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 }>
}