TASKS-1832

This commit is contained in:
Igor Martyniuk
2026-08-31 12:38:14 +00:00
parent 93fc6ff5a0
commit ae34b30e4f
25 changed files with 293 additions and 98 deletions
@@ -1,11 +1,11 @@
import { HttpContext } from '@adonisjs/core/http'
import type { HttpContext } from '@adonisjs/core/http'
import listValidator from '#base_modules/FileManager/validator/list_validator'
import uploadFileValidator from '#base_modules/FileManager/validator/upload_file_validator'
import createDirectoryValidator from '#base_modules/FileManager/validator/create_directory_validator'
import renameEntryValidator from '#base_modules/FileManager/validator/rename_entry_validator'
import duplicateEntriesValidator from '#base_modules/FileManager/validator/duplicate_entries_validator'
import FileMangerPolicy from '#base_modules/FileManager/policy/file_manager_policy'
import { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import type { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import getValidatedPathFromRequest from '#base_modules/FileManager/helpers/get_validated_path_from_request'
import FileManagerService from '#base_modules/FileManager/services/file_manager_service'
@@ -1,4 +1,4 @@
import { Entry } from '#base_modules/FileManager/types/entries'
import type { Entry } from '#base_modules/FileManager/types/entries'
export type EntryFilter = 'media' | 'archives' | 'documents' | 'all'
export type EntrySortBy = 'name' | 'size' | 'type' | 'date'
@@ -1,12 +1,12 @@
import { MultipartFile } from '@adonisjs/core/bodyparser'
import { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import type { MultipartFile } from '@adonisjs/core/bodyparser'
import type { Entry, FileEntry, FolderEntry } from '#base_modules/FileManager/types/entries'
import {
default as EntryListService,
EntryListParams,
EntryListResult,
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 {
import type {
FileStream,
StorageService,
} from '#base_modules/FileManager/services/storage_service/storage_service_interface'
@@ -1,4 +1,4 @@
import { FileEntry } from '#base_modules/FileManager/types/entries'
import type { FileEntry } from '#base_modules/FileManager/types/entries'
const PREVIEWABLE_TEXT_EXTENSIONS = ['txt', 'md', 'log', 'json', 'csv']
@@ -1,5 +1,5 @@
import env from '#start/env'
import { StorageService } from './storage_service_interface.js'
import type { StorageService } from './storage_service_interface.js'
import { FSStorageService } from './fs_storage_service.js'
import { S3StorageService } from './s3_storage_service.js'
@@ -4,9 +4,9 @@ import {
DirNotFoundException,
FileNotFoundException,
} from '#exceptions/file_manager_exceptions'
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 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 {
@@ -14,21 +14,41 @@ export class FSStorageService implements StorageService {
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) {
const response = await this.disk.listAll(oldPath, { recursive: true })
await this.disk.put(this.directoryMarkerService.markerPath(newPath), '')
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('/')
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(item.key, target)
await this.disk.move(file.key, target)
}
await this.disk.deleteAll(oldPath)
@@ -43,7 +63,15 @@ export class FSStorageService implements StorageService {
}
private async directoryExists(path: string): Promise<boolean> {
return this.disk.exists(this.directoryMarkerService.markerPath(path))
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(
@@ -226,11 +254,7 @@ export class FSStorageService implements StorageService {
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')
return this.getDirectoryEntry(path)
}
public writeFile: StorageService['writeFile'] = async (path, file) => {
@@ -5,9 +5,9 @@ import {
DirNotFoundException,
FileNotFoundException,
} from '#exceptions/file_manager_exceptions'
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 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 {
@@ -19,23 +19,38 @@ export class S3StorageService implements StorageService {
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) {
const oldPrefix = this.directoryPrefix(oldPath)
const response = await this.disk.listAll(oldPrefix, { recursive: true })
await this.disk.put(this.directoryMarkerService.markerPath(newPath), '')
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('/')
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(item.key, target)
await this.disk.move(file.key, target)
}
await this.disk.deleteAll(oldPrefix)
}
public entryExists: StorageService['entryExists'] = async (path) => {
@@ -47,7 +62,15 @@ export class S3StorageService implements StorageService {
}
private async directoryExists(path: string): Promise<boolean> {
return this.disk.exists(this.directoryMarkerService.markerPath(path))
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(
@@ -157,7 +180,9 @@ export class S3StorageService implements StorageService {
if (entry.type === 'file') {
await this.disk.delete(storagePath)
} else {
await this.disk.deleteAll(this.directoryPrefix(storagePath))
for (const file of await this.listDirectoryFiles(storagePath)) {
await this.disk.delete(file.key)
}
}
return { path }
@@ -238,11 +263,7 @@ export class S3StorageService implements StorageService {
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')
return this.getDirectoryEntry(path)
}
public writeFile: StorageService['writeFile'] = async (path, file) => {
@@ -1,6 +1,6 @@
import { Readable } from 'node:stream'
import { MultipartFile } from '@adonisjs/core/bodyparser'
import { Entry, FileEntry } from '#base_modules/FileManager/types/entries'
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