feat: added internal logic for managing directories, files and tags, reading, writing and creating of repositories

This commit is contained in:
overflowerror 2021-10-06 15:15:00 +02:00
parent bbd189ef59
commit 7ed8d49f55
7 changed files with 319 additions and 0 deletions

View file

@ -0,0 +1,5 @@
const KEY_DIRECTORIES = "directories"
const KEY_FILES = "files"
const KEY_FILE_PATH = "path"
const KEY_FILE_TAGS = "tags"

73
src/database/database.ts Normal file
View file

@ -0,0 +1,73 @@
class Database {
private directories: Directory[]
private files: MediaFile[]
constructor() {
}
public query(): Query {
return new Query(this.files)
}
public addDirectory(path: string) {
this.directories.push(new Directory(path))
}
public updateFile(file: MediaFile) {
this.files = this.files.filter(f => f.getPath() != file.getPath())
this.files.push(file)
}
public findUntaggedPathsInDb(): string[] {
return this.files
.filter(f => f.getTags().length == 0)
.map(f => f.getPath())
}
public async forEachDirectory(callback: (d: string) => Promise<void>): Promise<void> {
for (let i = 0; i < this.directories.length; i++) {
await callback(this.directories[i].getPath())
}
}
public hasFile(path: string): boolean {
return this.files.filter(f => f.getPath() == path).length != 0
}
public getFile(path: string): MediaFile {
const files = this.files.filter(f => f.getPath())
if (files.length == 1) {
return files[0]
} else {
return new MediaFile(path)
}
}
public static fromRaw(obj: any): Database {
const db = new Database()
if (!obj[KEY_DIRECTORIES] || typeof obj[KEY_DIRECTORIES] != "object") {
throw "directories attribute not found"
}
db.directories = obj[KEY_DIRECTORIES].map((d: any) => Directory.fromRaw(d))
if (!obj[KEY_FILES] || typeof obj[KEY_FILES] != "object") {
throw "files attribute not found"
}
db.files = obj[KEY_FILES].map((d: any) => MediaFile.fromRaw(d))
return db
}
public toRaw(): any {
let obj = {}
// @ts-ignore
obj[KEY_DIRECTORIES] = this.directories.map(d => d.toRaw())
// @ts-ignore
obj[KEY_FILES] = this.files.map(f => f.toRaw())
return obj
}
}

23
src/database/directory.ts Normal file
View file

@ -0,0 +1,23 @@
class Directory {
private readonly path: string
constructor(path: string) {
this.path = path
}
public getPath(): string {
return this.path
}
static fromRaw(obj: any): Directory {
if (typeof obj != "string") {
throw "directory has to be string"
}
return new Directory(obj)
}
public toRaw(): any {
return this.path
}
}

31
src/database/file-type.ts Normal file
View file

@ -0,0 +1,31 @@
enum MediaFileType {
unknown,
video,
image
}
function FileTypeFromExtension(name: string): MediaFileType {
while (name.indexOf(".") >= 0) {
name = name.substring(name.indexOf(".") + 1)
}
name = name.toLowerCase()
switch (name) {
case "jpeg":
case "jpg":
case "gif":
case "png":
case "webp":
return MediaFileType.image
case "mp4":
case "avi":
case "mpeg":
case "mpeg2":
case "m4v":
case "mkv":
case "mov":
case "webv":
return MediaFileType.video
default:
return MediaFileType.unknown
}
}

71
src/database/file.ts Normal file
View file

@ -0,0 +1,71 @@
class MediaFile {
private readonly path: string
private readonly type: MediaFileType
private tags: string[]
constructor(path: string) {
this.path = path
this.type = FileTypeFromExtension(path)
}
public getPath(): string {
return this.path
}
public getType(): MediaFileType {
return this.type
}
public getTags(): string[] {
return this.tags
}
public addTag(tag: string) {
if (this.tags.indexOf(tag) < 0) {
this.tags.push(tag)
}
}
public removeTag(tag: string) {
const i = this.tags.indexOf(tag)
if (i >= 0) {
this.tags.slice(i, 1)
}
}
public hasTag(tag: string): boolean {
return this.tags.indexOf(tag) >= 0
}
static fromRaw(obj: any): MediaFile {
if (typeof obj[KEY_FILE_PATH] != "string") {
throw "path attribute has to be string"
}
const file = new MediaFile(obj[KEY_FILE_PATH])
if (!obj[KEY_FILE_TAGS]) {
throw "tags attribute missing"
}
file.tags = obj[KEY_FILE_TAGS].map((t: any) => {
if (typeof t != "string") {
throw "tags have to be strings"
}
return t
})
return file
}
public toRaw(): any {
const obj = {}
// @ts-ignore
obj[KEY_FILE_PATH] = this.path
// @ts-ignore
obj[KEY_FILE_TAGS] = this.tags
return obj
}
}

56
src/database/query.ts Normal file
View file

@ -0,0 +1,56 @@
type CountedTag = {
name: string,
count: number
}
class Query {
private readonly files: MediaFile[]
public constructor(files: MediaFile[]) {
this.files = [...files]
}
public tags(): string[] {
return this.files
.map(f => f.getTags())
.flat()
.filter((v, i, s) => s.indexOf(v) == i)
}
public countedTags(): CountedTag[] {
return this.tags()
.map(t => ({
name: t,
count: this.files
.filter(f => f.hasTag(t))
.length
}))
}
public count(): number {
return this.files.length
}
public has(tag: string): Query {
return new Query(
this.files.filter(f => f.hasTag(tag))
)
}
public hasNot(tag: string): Query {
return new Query(
this.files.filter(f => !f.hasTag(tag))
)
}
public is(type: MediaFileType): Query {
return new Query(
this.files.filter(f => f.getType() == type)
)
}
public get(): MediaFile[] {
return this.files
}
}

60
src/repository/index.ts Normal file
View file

@ -0,0 +1,60 @@
import * as fsPromises from "fs/promises";
const DB_FILE = ".tagdb.json"
class Repository {
private readonly path: string
private cached?: Database
constructor(path: string) {
this.path = path
}
public async get(): Promise<Database> {
if (!this.cached) {
const buffer = await fsPromises.readFile(this.path + "/" + DB_FILE)
const raw = JSON.parse(buffer.toString())
this.cached = Database.fromRaw(raw)
}
return this.cached
}
public async reload(): Promise<Database> {
this.cached = undefined
return this.get()
}
public async exists(): Promise<boolean> {
try {
await this.get()
return true
} catch (e) {
return false
}
}
public new(db: Database) {
this.cached = db
}
public async write(): Promise<void> {
await fsPromises.writeFile(this.path + "/" + DB_FILE, JSON.stringify(this.cached.toRaw()))
}
public async findUntagged(): Promise<string[]> {
const db = await this.get()
const untagged = db.findUntaggedPathsInDb()
await db.forEachDirectory(async d => {
const entries = await fsPromises.readdir(this.path + "/" + d, {withFileTypes: true})
entries
.filter(e => e.isFile())
.map(e => e.name)
.filter(f => FileTypeFromExtension(f) != MediaFileType.unknown)
.filter(f => !db.hasFile(f))
.forEach(f => untagged.push(f))
})
return untagged.filter((v, i, s) => s.indexOf(v) == i)
}
}