207 lines
6.2 KiB
TypeScript
207 lines
6.2 KiB
TypeScript
import * as fs from '@std/fs';
|
|
import { FSDB_INDEXER, FSDB_SEARCH_OPTIONS } from '../fsdb.ts';
|
|
import * as path from '@std/path';
|
|
import sanitize from '../utils/sanitize.ts';
|
|
|
|
interface FSDB_INDEXER_SYMLINKS_CONFIG_SHARED {
|
|
name: string;
|
|
root?: string;
|
|
id_field?: string;
|
|
to_many?: boolean;
|
|
organize?: (value: string) => string[];
|
|
}
|
|
|
|
interface FSDB_INDEXER_SYMLINKS_CONFIG_WITH_FIELD extends FSDB_INDEXER_SYMLINKS_CONFIG_SHARED {
|
|
field: string;
|
|
get_values_to_index?: never;
|
|
}
|
|
|
|
interface FSDB_INDEXER_SYMLINKS_CONFIG_WITH_GET_VALUE<T> extends FSDB_INDEXER_SYMLINKS_CONFIG_SHARED {
|
|
field?: never;
|
|
get_values_to_index: (item: T) => string[];
|
|
}
|
|
|
|
export type FSDB_INDEXER_SYMLINKS_CONFIG<T> = FSDB_INDEXER_SYMLINKS_CONFIG_WITH_FIELD | FSDB_INDEXER_SYMLINKS_CONFIG_WITH_GET_VALUE<T>;
|
|
|
|
async function cleanup_empty_directories(initial_path: string): Promise<string[]> {
|
|
let current_directory: string = path.dirname(path.resolve(initial_path));
|
|
let has_children: boolean = false;
|
|
const removed: string[] = [];
|
|
do {
|
|
for await (const _dir_entry of Deno.readDir(current_directory)) {
|
|
has_children = true;
|
|
break;
|
|
}
|
|
|
|
if (!has_children) {
|
|
const removed_dir = current_directory;
|
|
await Deno.remove(removed_dir);
|
|
current_directory = path.dirname(removed_dir);
|
|
removed.push(removed_dir);
|
|
if (current_directory === Deno.cwd()) {
|
|
break;
|
|
}
|
|
}
|
|
} while (!has_children && current_directory.length);
|
|
|
|
return removed;
|
|
}
|
|
|
|
export class FSDB_INDEXER_SYMLINKS<T> implements FSDB_INDEXER<T> {
|
|
constructor(private config: FSDB_INDEXER_SYMLINKS_CONFIG<T>) {
|
|
this.config.id_field = this.config.id_field ?? 'id';
|
|
}
|
|
|
|
public set_fsdb_root(root: string) {
|
|
this.config.root = this.config.root ?? path.resolve(path.join(root, '.indexes/', sanitize(this.config.name)));
|
|
}
|
|
|
|
private get_values_to_index(item: any): string[] {
|
|
if (this.config.get_values_to_index) {
|
|
return this.config.get_values_to_index(item);
|
|
}
|
|
|
|
const typed_field: any = this.config.field as keyof T;
|
|
|
|
const value: string = item[typed_field];
|
|
return [value];
|
|
}
|
|
|
|
async lookup(value: string, options?: FSDB_SEARCH_OPTIONS): Promise<string[]> {
|
|
if (typeof this.config.root !== 'string') {
|
|
throw new Error('root should have been set by FSDB instance');
|
|
}
|
|
|
|
if (typeof this.config.organize === 'undefined') {
|
|
throw new Error('symlink indexer must have an organizer set!');
|
|
}
|
|
|
|
const results: string[] = [];
|
|
|
|
const organized_paths: string[] = this.config.organize(value);
|
|
if (organized_paths.length === 0) {
|
|
return results;
|
|
}
|
|
|
|
if (this.config.to_many) {
|
|
const filename: string = organized_paths.pop() ?? ''; // remove filename
|
|
const parsed_filename = path.parse(filename);
|
|
organized_paths.push(parsed_filename.name); // add back filename without extension for a directory
|
|
organized_paths.push('*'); // wildcard to get all references
|
|
}
|
|
|
|
const limit = options?.limit ?? 100;
|
|
const offset = options?.offset ?? 0;
|
|
let counter = 0;
|
|
|
|
const glob_pattern = path.resolve(path.join(this.config.root, ...organized_paths));
|
|
for await (const item_file of fs.expandGlob(glob_pattern)) {
|
|
const file_info: Deno.FileInfo = await Deno.lstat(item_file.path);
|
|
if (file_info.isSymlink) {
|
|
if (counter < offset) {
|
|
++counter;
|
|
continue;
|
|
}
|
|
|
|
const resolved_item_path = await Deno.readLink(item_file.path);
|
|
results.push(resolved_item_path);
|
|
++counter;
|
|
}
|
|
|
|
if (counter >= (offset + limit)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async index(item: T, authoritative_path: string): Promise<string[]> {
|
|
if (typeof this.config.root !== 'string') {
|
|
throw new Error('root should have been set by FSDB instance');
|
|
}
|
|
|
|
if (typeof this.config.organize === 'undefined') {
|
|
throw new Error('symlink indexer must have an organizer set!');
|
|
}
|
|
|
|
const results: string[] = [];
|
|
const values: string[] = this.get_values_to_index(item);
|
|
if (values.length === 0) {
|
|
return results;
|
|
}
|
|
|
|
for (const value of values) {
|
|
const organized_paths: string[] = this.config.organize(value);
|
|
if (organized_paths.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (this.config.to_many) {
|
|
const filename: string = organized_paths.pop() ?? ''; // remove filename
|
|
const parsed_filename = path.parse(filename);
|
|
organized_paths.push(parsed_filename.name); // add back filename without extension for a directory
|
|
|
|
const item_id: string = item[this.config.id_field as keyof T] as string;
|
|
if (typeof item_id !== 'string') {
|
|
continue;
|
|
}
|
|
|
|
organized_paths.push(`${item_id}.json`);
|
|
}
|
|
|
|
const symlink_path = path.resolve(path.join(this.config.root, ...organized_paths));
|
|
const item_dir = path.dirname(authoritative_path);
|
|
const reverse_link_path = path.join(item_dir, `.index.symlink.${this.config.name}.${sanitize(value)}`);
|
|
|
|
// clean up old indexes
|
|
try {
|
|
const previous_symlink_index_link_path = await Deno.readLink(reverse_link_path);
|
|
await Deno.remove(previous_symlink_index_link_path);
|
|
|
|
if (this.config.to_many) {
|
|
await cleanup_empty_directories(previous_symlink_index_link_path);
|
|
}
|
|
|
|
await Deno.remove(reverse_link_path);
|
|
} catch (error) {
|
|
if (!(error instanceof Deno.errors.NotFound)) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// create index symlink and reverse link
|
|
await Deno.mkdir(path.dirname(symlink_path), { recursive: true });
|
|
await fs.ensureSymlink(authoritative_path, symlink_path);
|
|
await fs.ensureSymlink(symlink_path, reverse_link_path);
|
|
|
|
results.push(symlink_path);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
async remove(item: T, item_path: string): Promise<string[]> {
|
|
const results: string[] = [];
|
|
const values: string[] = this.get_values_to_index(item);
|
|
const item_dir: string = path.dirname(item_path);
|
|
|
|
for (const value of values) {
|
|
const item_dir_reverse_link: string = path.join(item_dir, `.index.symlink.${this.config.name}.${sanitize(value)}`);
|
|
if (!fs.existsSync(item_dir_reverse_link)) {
|
|
continue;
|
|
}
|
|
|
|
const index_symlink_path: string = await Deno.readLink(item_dir_reverse_link);
|
|
if (fs.existsSync(index_symlink_path)) {
|
|
await Deno.remove(index_symlink_path);
|
|
results.push(index_symlink_path);
|
|
}
|
|
|
|
await Deno.remove(item_dir_reverse_link);
|
|
results.push(...await cleanup_empty_directories(index_symlink_path));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|