feature: initial commit
This commit is contained in:
commit
ce024ba87a
17 changed files with 1141 additions and 0 deletions
207
indexers/symlinks.ts
Normal file
207
indexers/symlinks.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue