categorys and titles for images

This commit is contained in:
Zutatensuppe 2021-05-22 01:51:44 +02:00
parent 8abbb13fcc
commit 239c879649
22 changed files with 964 additions and 86 deletions

212
src/server/Db.ts Normal file
View file

@ -0,0 +1,212 @@
import fs from 'fs'
import bsqlite from 'better-sqlite3'
import Integer from 'integer'
import { logger } from '../common/Util'
const log = logger('Db.ts')
/**
* TODO: create a more specific type for OrderBy.
* It looks like this (example):
* [
* {id: -1}, // by id descending
* {name: 1}, // then by name ascending
* ]
*/
type OrderBy = Array<any>
type Data = Record<string, any>
type WhereRaw = Record<string, any>
type Params = Array<any>
interface Where {
sql: string
values: Array<any>
}
class Db {
file: string
patchesDir: string
dbh: bsqlite.Database
constructor(file: string, patchesDir: string) {
this.file = file
this.patchesDir = patchesDir
this.dbh = bsqlite(this.file)
}
close(): void {
this.dbh.close()
}
patch (verbose: boolean =true): void {
if (!this.get('sqlite_master', {type: 'table', name: 'db_patches'})) {
this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', [])
}
const files = fs.readdirSync(this.patchesDir)
const patches = (this.getMany('db_patches')).map(row => row.id)
for (const f of files) {
if (patches.includes(f)) {
if (verbose) {
log.info(`➡ skipping already applied db patch: ${f}`)
}
continue
}
const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8')
const all = contents.split(';').map(s => s.trim()).filter(s => !!s)
try {
this.dbh.transaction((all) => {
for (const q of all) {
if (verbose) {
log.info(`Running: ${q}`)
}
this.run(q)
}
this.insert('db_patches', {id: f})
})(all)
log.info(`✓ applied db patch: ${f}`)
} catch (e) {
log.error(`✖ unable to apply patch: ${f} ${e}`)
return
}
}
}
_buildWhere (where: WhereRaw): Where {
const wheres = []
const values = []
for (const k of Object.keys(where)) {
if (where[k] === null) {
wheres.push(k + ' IS NULL')
continue
}
if (typeof where[k] === 'object') {
let prop = '$nin'
if (where[k][prop]) {
if (where[k][prop].length > 0) {
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')')
values.push(...where[k][prop])
}
continue
}
prop = '$in'
if (where[k][prop]) {
if (where[k][prop].length > 0) {
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')')
values.push(...where[k][prop])
}
continue
}
// TODO: implement rest of mongo like query args ($eq, $lte, $in...)
throw new Error('not implemented: ' + JSON.stringify(where[k]))
}
wheres.push(k + ' = ?')
values.push(where[k])
}
return {
sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '',
values,
}
}
_buildOrderBy (orderBy: OrderBy): string {
const sorts = []
for (const s of orderBy) {
const k = Object.keys(s)[0]
sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC'))
}
return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : ''
}
_get (query: string, params: Params = []): any {
return this.dbh.prepare(query).get(...params)
}
run (query: string, params: Params = []): bsqlite.RunResult {
return this.dbh.prepare(query).run(...params)
}
_getMany (query: string, params: Params = []): Array<any> {
return this.dbh.prepare(query).all(...params)
}
get (
table: string,
whereRaw: WhereRaw = {},
orderBy: OrderBy = []
): any {
const where = this._buildWhere(whereRaw)
const orderBySql = this._buildOrderBy(orderBy)
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
return this._get(sql, where.values)
}
getMany (
table: string,
whereRaw: WhereRaw = {},
orderBy: OrderBy = []
): Array<any> {
const where = this._buildWhere(whereRaw)
const orderBySql = this._buildOrderBy(orderBy)
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
return this._getMany(sql, where.values)
}
delete (table: string, whereRaw: WhereRaw = {}): bsqlite.RunResult {
const where = this._buildWhere(whereRaw)
const sql = 'DELETE FROM ' + table + where.sql
return this.run(sql, where.values)
}
exists (table: string, whereRaw: WhereRaw): boolean {
return !!this.get(table, whereRaw)
}
upsert (
table: string,
data: Data,
check: WhereRaw,
idcol: string|null = null
): any {
if (!this.exists(table, check)) {
return this.insert(table, data)
}
this.update(table, data, check)
if (idcol === null) {
return 0 // dont care about id
}
return this.get(table, check)[idcol] // get id manually
}
insert (table: string, data: Data): Integer.IntLike {
const keys = Object.keys(data)
const values = keys.map(k => data[k])
const sql = 'INSERT INTO '+ table
+ ' (' + keys.join(',') + ')'
+ ' VALUES (' + keys.map(k => '?').join(',') + ')'
return this.run(sql, values).lastInsertRowid
}
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
const keys = Object.keys(data)
if (keys.length === 0) {
return
}
const values = keys.map(k => data[k])
const setSql = ' SET ' + keys.join(' = ?,') + ' = ?'
const where = this._buildWhere(whereRaw)
const sql = 'UPDATE ' + table + setSql + where.sql
this.run(sql, [...values, ...where.values])
}
}
export default Db

View file

@ -10,3 +10,6 @@ export const DATA_DIR = `${BASE_DIR}/data`
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
export const UPLOAD_URL = `/uploads`
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
export const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`
export const DB_FILE = `${BASE_DIR}/data/db.sqlite`

View file

@ -4,6 +4,7 @@ import exif from 'exif'
import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
import Db from './Db'
const resizeImage = async (filename: string) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
@ -46,16 +47,76 @@ async function getExifOrientation(imagePath: string) {
})
}
const allImages = (sort: string) => {
const getCategories = (db: Db, imageId: number) => {
const query = `
select * from categories c
inner join image_x_category ixc on c.id = ixc.category_id
where ixc.image_id = ?`
return db._getMany(query, [imageId])
}
const imageFromDb = (db: Db, imageId: number) => {
const i = db.get('images', { id: imageId })
return {
id: i.id,
filename: i.filename,
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
categories: getCategories(db, i.id) as any[],
created: i.created * 1000,
}
}
const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
const sortMap = {
alpha_asc: [{filename: 1}],
alpha_desc: [{filename: -1}],
date_asc: [{created: 1}],
date_desc: [{created: -1}],
} as Record<string, any>
// TODO: .... clean up
const wheresRaw: Record<string, any> = {}
if (categorySlug !== '') {
const c = db.get('categories', {slug: categorySlug})
if (!c) {
return []
}
const ids = db._getMany(`
select i.id from image_x_category ixc
inner join images i on i.id = ixc.image_id
where ixc.category_id = ?;
`, [c.id]).map(img => img.id)
if (ids.length === 0) {
return []
}
wheresRaw['id'] = {'$in': ids}
}
const images = db.getMany('images', wheresRaw, sortMap[sort])
return images.map(i => ({
id: i.id as number,
filename: i.filename,
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
categories: getCategories(db, i.id) as any[],
created: i.created * 1000,
}))
}
const allImagesFromDisk = (category: string, sort: string) => {
let images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({
id: 0,
filename: f,
file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
title: '',
category: '',
ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
title: f.replace(/\.[a-z]+$/, ''),
categories: [] as any[],
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
}))
switch (sort) {
@ -73,14 +134,14 @@ const allImages = (sort: string) => {
case 'date_asc':
images = images.sort((a, b) => {
return a.ts > b.ts ? 1 : -1
return a.created > b.created ? 1 : -1
})
break;
case 'date_desc':
default:
images = images.sort((a, b) => {
return a.ts < b.ts ? 1 : -1
return a.created < b.created ? 1 : -1
})
break;
}
@ -102,7 +163,9 @@ async function getDimensions(imagePath: string) {
}
export default {
allImages,
allImagesFromDisk,
imageFromDb,
allImagesFromDb,
resizeImage,
getDimensions,
}

View file

@ -12,9 +12,19 @@ import GameLog from './GameLog'
import GameSockets from './GameSockets'
import Time from './../common/Time'
import Images from './Images'
import { UPLOAD_DIR, UPLOAD_URL, PUBLIC_DIR } from './Dirs'
import {
DB_FILE,
DB_PATCHES_DIR,
PUBLIC_DIR,
UPLOAD_DIR,
UPLOAD_URL
} from './Dirs'
import { GameSettings, ScoreMode } from '../common/GameCommon'
import GameStorage from './GameStorage'
import Db from './Db'
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch()
let configFile = ''
let last = ''
@ -54,8 +64,8 @@ app.get('/api/conf', (req, res) => {
app.get('/api/newgame-data', (req, res) => {
const q = req.query as any
res.send({
images: Images.allImages(q.sort),
categories: [],
images: Images.allImagesFromDb(db, q.category, q.sort),
categories: db.getMany('categories', {}, [{ title: 1 }]),
})
})
@ -94,12 +104,26 @@ app.post('/upload', (req, res) => {
res.status(400).send("Something went wrong!");
}
res.send({
image: {
file: `${UPLOAD_DIR}/${req.file.filename}`,
url: `${UPLOAD_URL}/${req.file.filename}`,
},
const imageId = db.insert('images', {
filename: req.file.filename,
filename_original: req.file.originalname,
title: req.body.title || '',
created: Time.timestamp(),
})
if (req.body.category) {
const title = req.body.category
const slug = Util.slug(title)
const id = db.upsert('categories', { slug, title }, { slug }, 'id')
if (id) {
db.insert('image_x_category', {
image_id: imageId,
category_id: id,
})
}
}
res.send(Images.imageFromDb(db, imageId as number))
})
})