TASKS-1832
This commit is contained in:
@@ -16,8 +16,13 @@ export default class AdminsController {
|
||||
search = null,
|
||||
sortBy = 'id',
|
||||
sortOrder = 'desc',
|
||||
status = null,
|
||||
} = request.all()
|
||||
const query = Admins.query()
|
||||
if (status) {
|
||||
const statuses = Array.isArray(status) ? status : [status]
|
||||
query.whereIn('status', statuses)
|
||||
}
|
||||
if (search) {
|
||||
const columns = Admins.searchable || (await searchableColumns('admins'))
|
||||
addSearchFilter(query, columns, search)
|
||||
|
||||
@@ -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
-1
@@ -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'
|
||||
|
||||
|
||||
+42
-18
@@ -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) => {
|
||||
|
||||
+41
-20
@@ -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) => {
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -136,7 +136,7 @@ export type ComponentContent =
|
||||
export type BlockData = ComponentContent['data']
|
||||
|
||||
export interface SiteMenu {
|
||||
title: string
|
||||
title?: string
|
||||
items: NavigationLink[]
|
||||
}
|
||||
|
||||
@@ -146,11 +146,11 @@ export interface SiteSettingsContent {
|
||||
ctaItems: NavigationLink[]
|
||||
}
|
||||
footer: {
|
||||
copyright: string
|
||||
copyright?: string
|
||||
menus: SiteMenu[]
|
||||
}
|
||||
legal: {
|
||||
privacyPath: string
|
||||
termsPath: string
|
||||
privacyPath?: string
|
||||
termsPath?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ const componentContentSchema = () =>
|
||||
|
||||
const siteMenuSchema = () =>
|
||||
vine.object({
|
||||
title: vine.string(),
|
||||
title: vine.string().optional(),
|
||||
items: vine.array(navigationLinkSchema()),
|
||||
})
|
||||
|
||||
@@ -175,12 +175,12 @@ const siteSettingsContentSchema = () =>
|
||||
ctaItems: vine.array(navigationLinkSchema()),
|
||||
}),
|
||||
footer: vine.object({
|
||||
copyright: vine.string(),
|
||||
copyright: vine.string().optional(),
|
||||
menus: vine.array(siteMenuSchema()),
|
||||
}),
|
||||
legal: vine.object({
|
||||
privacyPath: vine.string(),
|
||||
termsPath: vine.string(),
|
||||
privacyPath: vine.string().optional(),
|
||||
termsPath: vine.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { test } from '@japa/runner'
|
||||
import drive from '@adonisjs/drive/services/main'
|
||||
import { FSStorageService } from '#base_modules/FileManager/services/storage_service/fs_storage_service'
|
||||
|
||||
const disk = drive.use('fs')
|
||||
const storage = new FSStorageService()
|
||||
|
||||
async function cleanup() {
|
||||
for (const dir of ['aa', 'aab', 'cc', 'ghostdir', 'ghostdir2']) {
|
||||
await disk.deleteAll(dir)
|
||||
}
|
||||
}
|
||||
|
||||
test.group('FSStorageService directories', (group) => {
|
||||
group.each.setup(() => cleanup())
|
||||
group.each.teardown(() => cleanup())
|
||||
|
||||
test('finds a directory that has no .keep marker', async ({ assert }) => {
|
||||
await disk.put('ghostdir/a.txt', 'hello')
|
||||
|
||||
assert.isTrue(await storage.entryExists('ghostdir'))
|
||||
assert.deepInclude(await storage.getEntry('ghostdir'), { type: 'folder', name: 'ghostdir' })
|
||||
})
|
||||
|
||||
test('moves a marker-less directory with its contents', async ({ assert }) => {
|
||||
await disk.put('ghostdir/nested/a.txt', 'hello')
|
||||
|
||||
await storage.moveEntry('ghostdir', 'ghostdir2')
|
||||
|
||||
assert.isFalse(await disk.exists('ghostdir/nested/a.txt'))
|
||||
assert.equal(await disk.get('ghostdir2/nested/a.txt'), 'hello')
|
||||
assert.deepInclude(await storage.getEntry('ghostdir2'), { type: 'folder' })
|
||||
})
|
||||
|
||||
test('does not touch a sibling directory sharing the same name prefix', async ({ assert }) => {
|
||||
await disk.put('aa/a.txt', 'from aa')
|
||||
await disk.put('aab/a.txt', 'from aab')
|
||||
|
||||
await storage.moveEntry('aa', 'cc')
|
||||
|
||||
assert.equal(await disk.get('cc/a.txt'), 'from aa')
|
||||
assert.equal(await disk.get('aab/a.txt'), 'from aab')
|
||||
})
|
||||
|
||||
test('deletes a directory without touching a prefix sibling', async ({ assert }) => {
|
||||
await disk.put('aa/a.txt', 'from aa')
|
||||
await disk.put('aab/a.txt', 'from aab')
|
||||
|
||||
await storage.deleteEntry('aa')
|
||||
|
||||
assert.isFalse(await disk.exists('aa/a.txt'))
|
||||
assert.equal(await disk.get('aab/a.txt'), 'from aab')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user