From 37e0fbfc89d98cfa151140285aec1aa22c77f935 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 8 Dec 2024 17:31:17 +0800 Subject: [PATCH] add: functions to migrate database to V2 schema add: doc for database schema --- .tokeignore | 160 ++++++++++++++++ docs/database-changelog.md | 202 ++++++++++++++++++++ docs/database-structure.md | 169 ++++++++++++++++ package.json | 2 +- pages/rewind/index.tsx | 34 +--- pnpm-lock.yaml | 20 +- src/electron/backend/init.ts | 35 +++- src/electron/backend/migrate/index.ts | 11 ++ src/electron/backend/migrate/migrateToV2.ts | 81 ++++++++ src/electron/backend/screenshot.ts | 11 +- src/electron/index.ts | 5 + src/electron/utils/backend.ts | 6 +- 12 files changed, 677 insertions(+), 59 deletions(-) create mode 100644 .tokeignore create mode 100644 docs/database-changelog.md create mode 100644 docs/database-structure.md create mode 100644 src/electron/backend/migrate/index.ts create mode 100644 src/electron/backend/migrate/migrateToV2.ts diff --git a/.tokeignore b/.tokeignore new file mode 100644 index 0000000..6fb7b05 --- /dev/null +++ b/.tokeignore @@ -0,0 +1,160 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +bin + +*.yaml +*.json +*.md \ No newline at end of file diff --git a/docs/database-changelog.md b/docs/database-changelog.md new file mode 100644 index 0000000..f5eecd4 --- /dev/null +++ b/docs/database-changelog.md @@ -0,0 +1,202 @@ +# Database Schema Documentation + +This document outlines the changes made across different versions of +database structure used in the OpenRewind, including tables and fields. + +## Version 2 Schema Changes + +Cooresponding version: Since 0.4.0 + +### New Table: `config` + +Stores configuration data, including the database version. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|---------------------|-----------------------------------------------------------------------------| +| `key` | TEXT | PRIMARY KEY | Unique key for configuration settings. | +| `value` | TEXT | | Value associated with the key. | + +#### Insert Default Version + +```sql +INSERT INTO config (key, value) VALUES ('version', '2'); +``` + +### New Table: `encoding_task` + +Stores encoding tasks that are queued for processing. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|----------------------------|--------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique ID for the task. | +| `createAt` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Timestamp when the task was created. | +| `status` | INTEGER | DEFAULT 0 | Indicates the status of the task. | + +### Task status + +- `0`: Pending +- `1`: In Progress +- `2`: Completed + - Once the task was set to this status, it will be imminently deleted by a trigger mentioned below. + +### New Trigger: `delete_encoding_task` + +Triggered after updating the `status` of an encoding task to `2` (Completed). + +```sql +CREATE TRIGGER delete_encoding_task +AFTER UPDATE OF status +ON encoding_task +BEGIN + DELETE FROM encoding_task_data + WHERE encodingTaskID = OLD.id AND NEW.status = 2; + + DELETE FROM encoding_task + WHERE id = OLD.id AND NEW.status = 2; +END; + +``` + +### New Table: `encoding_task_data` + +Stores the frames that need to be encoded for the encoding task + +| Column Name | Data Type | Constraints/Default | Description | +|------------------|-----------|-------------------------------------|------------------------------------------------------| +| `frame` | INTEGER | PRIMARY KEY, FOREIGN KEY (frame.id) | ID for the frame associated with the encoding task. | +| `encodingTaskID` | TIMESTAMP | FOREIGN KEY (encoding_task.id) | ID for the encoding task associated with this frame. | + + +### Update `frame` Table + +#### Simplify `imgFilename` + +The `imgFilename` column was updated to store only the filename without the full path. + +```typescript +const rows = db.prepare('SELECT id, imgFilename FROM frame').all() as OldFrame[]; +rows.forEach(row => { + const filename = row.imgFilename.match(/[^\\/]+$/)?.[0]; + if (filename) { + db.prepare('UPDATE frame SET imgFilename = ? WHERE id = ?').run(filename, row.id); + } +}); +``` + +#### Add `encodeStatus` Column + +A new column `encodeStatus` was added to replace the deprecated `encoded` column. + +```sql +ALTER TABLE frame ADD encodeStatus INT; +UPDATE frame SET encodeStatus = CASE WHEN encoded THEN 2 ELSE 0 END; +``` + +### Summary of Changes + +- **New Table:** `config` to store configuration data. +- **Update `frame` Table:** + - Simplified `imgFilename` to store only the filename. + - Added `encodeStatus` column to replace the deprecated `encoded` column. +- **Deprecated `encoded` column.** + - The `encoded` column is no longer used and is retained due to SQLite's inability to drop columns. + Creating a new table without this column and copying data to the new table could be time-consuming. + + +## Version 1 Schema + +Cooresponding version: 0.3.x + +### Table: `frame` + +Stores information about individual frames. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------------|-----------|---------------------------------|-------------------------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each frame. | +| `createAt` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Timestamp when the frame was created. | +| `imgFilename` | TEXT | | Filename of the image associated with the frame. | +| `segmentID` | INTEGER | NULL, FOREIGN KEY (segments.id) | ID of the segment to which the frame belongs. | +| `videoPath` | TEXT | NULL | Path to the video file if the frame is part of a video. | +| `videoFrameIndex` | INTEGER | NULL | Index of the frame within the video. | +| `collectionID` | INTEGER | NULL | ID of the collection to which the frame belongs. | +| `encoded` | BOOLEAN | DEFAULT 0 | Indicates whether the frame has been encoded (0 for false, 1 for true). | + +### Table: `recognition_data` + +Stores recognition data associated with frames. + +| Column Name | Data Type | Constraints/Default | Desc[database-structure.md](database-structure.md)ription | +|-------------|-----------|----------------------------|-----------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each recognition data entry. | +| `frameID` | INTEGER | FOREIGN KEY (frame.id) | ID of the frame to which the recognition data belongs. | +| `data` | TEXT | | Raw recognition data. | +| `text` | TEXT | | Recognized text. | + +### Table: `segments` + +A segment is a period of time when a user uses an application. +While capturing the screen, OpenRewind retrieves the currently active window. +When it finds that the currently active window has changed to another application, a new segment will start. + +| Column Name | Data Type | Constraints/Default | Description | +|---------------|-----------|----------------------------|------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each segment. | +| `startAt` | TIMESTAMP | | Timestamp when the segment starts. | +| `endAt` | TIMESTAMP | | Timestamp when the segment ends. | +| `title` | TEXT | | Title of the segment. | +| `appName` | TEXT | | Name of the application associated with the segment. | +| `appPath` | TEXT | | Path to the application. | +| `text` | TEXT | | Text content of the segment. | +| `type` | TEXT | | Type of the segment. | +| `appBundleID` | TEXT | NULL | Bundle ID of the application. | +| `url` | TEXT | NULL | URL associated with the segment. | + +### Virtual Table: `text_search` + +Used for full-text search on recognition data. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|---------------------|--------------------------------------------------------| +| `id` | INTEGER | UNINDEXED | ID of the recognition data entry. | +| `frameID` | INTEGER | UNINDEXED | ID of the frame to which the recognition data belongs. | +| `data` | TEXT | | Raw recognition data. | +| `text` | TEXT | | Recognized text. | + +### Triggers + +#### `recognition_data_after_insert` + +Triggered after inserting a new row into `recognition_data`. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_insert AFTER INSERT ON recognition_data +BEGIN + INSERT INTO text_search (id, frameID, data, text) + VALUES (NEW.id, NEW.frameID, NEW.data, NEW.text); +END; +``` + +#### `recognition_data_after_update` + +Triggered after updating a row in `recognition_data`. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_update AFTER UPDATE ON recognition_data +BEGIN + UPDATE text_search + SET frameID = NEW.frameID, data = NEW.data, text = NEW.text + WHERE id = NEW.id; +END; +``` + +#### `recognition_data_after_delete` + +Triggered after deleting a row from `recognition_data`. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_delete AFTER DELETE ON recognition_data +BEGIN + DELETE FROM text_search WHERE id = OLD.id; +END; +``` diff --git a/docs/database-structure.md b/docs/database-structure.md new file mode 100644 index 0000000..4b9b580 --- /dev/null +++ b/docs/database-structure.md @@ -0,0 +1,169 @@ +# Database Schema Documentation (Version 2) + +This document outlines the current structure of the database schema used in the application. +It includes tables, fields, and their descriptions. + +## Table: `config` + +Stores configuration data. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|---------------------|-----------------------------------------------------------------------------| +| `key` | TEXT | PRIMARY KEY | Unique key for configuration settings. | +| `value` | TEXT | | Value associated with the key. | + +### Key: version + +The current database schema version, represented as a integer. +Since the `config` table does not exist in V1, the version must be at least 2. + + +## Table: `frame` + +Stores information about individual frames. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------------|-----------|---------------------------------|-----------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each frame. | +| `createAt` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Timestamp when the frame was created. | +| `imgFilename` | TEXT | | Filename of the image associated with the frame. | +| `segmentID` | INTEGER | NULL, FOREIGN KEY (segments.id) | ID of the segment to which the frame belongs. | +| `videoPath` | TEXT | NULL | Relative path to the video file if the frame was encoded. | +| `videoFrameIndex` | INTEGER | NULL | Index of the frame within the encoded video. | +| `collectionID` | INTEGER | NULL | ID of the collection to which the frame belongs. | +| `encodeStatus` | INTEGER | 0 | Indicates the encoding status of the frame. | + + +### Status Description + +- `0`: The frame is not encoded. +- `1`: The frame is scheduled for encoding. It will appear in the `encoding_task` table. +- `2`: The frame is already encoded. + +## Table: `recognition_data` + +Stores recognition data associated with frames. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|------------------------------|-----------------------------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each recognition data entry. | +| `frameID` | INTEGER | FOREIGN KEY (frame.id) | ID of the frame to which the recognition data belongs. | +| `data` | TEXT | | Raw recognition data. | +| `text` | TEXT | | Recognized text. | + +## Table: `segments` + +A segment is a period of time when a user uses a particular application. +While capturing the screen, OpenRewind detects the currently active window. +When it finds that the currently active window has changed to another application, a new segment will start. + +| Column Name | Data Type | Constraints/Default | Description | +|---------------|-----------|----------------------------|------------------------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique identifier for each segment. | +| `startAt` | TIMESTAMP | | Timestamp when the segment starts. | +| `endAt` | TIMESTAMP | | Timestamp when the segment ends. | +| `title` | TEXT | | Title of the segment. | +| `appName` | TEXT | | Name of the application associated with the segment. | +| `appPath` | TEXT | | Path to the application. | +| `text` | TEXT | | Text content of the segment. | +| `type` | TEXT | | Type of the segment. | +| `appBundleID` | TEXT | NULL | Bundle ID of the application. | +| `url` | TEXT | NULL | URL associated with the segment. | + +## Table: `encoding_task` + +Stores encoding tasks that are queued for processing. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|----------------------------|--------------------------------------| +| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | Unique ID for the task. | +| `createAt` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Timestamp when the task was created. | +| `status` | INTEGER | DEFAULT 0 | Indicates the status of the task. | + +### Task status Description + +- `0`: Pending +- `1`: In Progress +- `2`: Completed + - Once the task was set to this status, it will be imminently deleted by a trigger mentioned below. + +## Table: `encoding_task_data` + +Stores the frames that need to be encoded for the encoding task + +| Column Name | Data Type | Constraints/Default | Description | +|------------------|-----------|-------------------------------------|------------------------------------------------------| +| `frame` | INTEGER | PRIMARY KEY, FOREIGN KEY (frame.id) | ID for the frame associated with the encoding task. | +| `encodingTaskID` | TIMESTAMP | FOREIGN KEY (encoding_task.id) | ID for the encoding task associated with this frame. | + + +## Virtual Table: `text_search` + +Used for full-text search on recognition data. + +| Column Name | Data Type | Constraints/Default | Description | +|-------------|-----------|---------------------|--------------------------------------------------------| +| `id` | INTEGER | UNINDEXED | ID of the recognition data entry. | +| `frameID` | INTEGER | UNINDEXED | ID of the frame to which the recognition data belongs. | +| `data` | TEXT | | Raw recognition data. | +| `text` | TEXT | | Recognized text. | + +## Triggers + +### `recognition_data_after_insert` + +Triggered after inserting a new row into `recognition_data`. +Inserts a new row into `text_search` with the same data. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_insert AFTER INSERT ON recognition_data +BEGIN + INSERT INTO text_search (id, frameID, data, text) + VALUES (NEW.id, NEW.frameID, NEW.data, NEW.text); +END; +``` + +### `recognition_data_after_update` + +Triggered after updating a row in `recognition_data`. +Updates the associated `text_search` row. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_update AFTER UPDATE ON recognition_data +BEGIN + UPDATE text_search + SET frameID = NEW.frameID, data = NEW.data, text = NEW.text + WHERE id = NEW.id; +END; +``` + +### `recognition_data_after_delete` + +Triggered after deleting a row from `recognition_data`. +Deletes the associated `text_search` row. + +```sql +CREATE TRIGGER IF NOT EXISTS recognition_data_after_delete AFTER DELETE ON recognition_data +BEGIN + DELETE FROM text_search WHERE id = OLD.id; +END; +``` + +### `delete_encoding_task` + +Triggered after updating the `status` of an encoding task to `2` (Completed). +Deletes the associated `encoding_task_data` and `encoding_task` rows. + +```sql +CREATE TRIGGER delete_encoding_task +AFTER UPDATE OF status +ON encoding_task +BEGIN + DELETE FROM encoding_task_data + WHERE encodingTaskID = OLD.id AND NEW.status = 2; + + DELETE FROM encoding_task + WHERE id = OLD.id AND NEW.status = 2; +END; + +``` diff --git a/package.json b/package.json index 11f60cc..b3e225a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openrewind", - "version": "0.3.1", + "version": "0.4.0", "type": "module", "description": "Your second brain, superpowered.", "main": "dist/electron/index.js", diff --git a/pages/rewind/index.tsx b/pages/rewind/index.tsx index 916b94c..e0dc47a 100644 --- a/pages/rewind/index.tsx +++ b/pages/rewind/index.tsx @@ -1,41 +1,11 @@ -import { useState } from "react"; import "./index.css"; export default function RewindPage() { - const [currentScreenShotBase64, setScreenShotData] = useState(null); - window.api.receive("fromMain", (message: string) => { - setScreenShotData(message); - }); + return ( <>
- {currentScreenShotBase64 && ( - - )} - {currentScreenShotBase64 ? ( -
- Here's a screenshot captured just now. -
- The relavant features has not been implemented, and you han hit Esc to quit - this window. -
- Meow! ヽ(=^・ω・^=)丿 -
- ) : ( -
- Now capturing a screenshot for your screen...(ง •̀_•́)ง -
- )} +
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfa6703..de17652 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: react-router-dom: specifier: ^7.0.1 version: 7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + screenshot-desktop: + specifier: ^1.15.0 + version: 1.15.0 sqlite3: specifier: ^5.1.7 version: 5.1.7 @@ -153,9 +156,6 @@ importers: postcss: specifier: ^8.4.38 version: 8.4.49 - screenshot-desktop: - specifier: ^1.15.0 - version: 1.15.0 tailwindcss: specifier: ^3.4.15 version: 3.4.16 @@ -4927,7 +4927,7 @@ snapshots: app-builder-bin@5.0.0-alpha.10: {} - app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.5.0 @@ -4943,7 +4943,7 @@ snapshots: chromium-pickle-js: 0.2.0 config-file-ts: 0.2.8-rc1 debug: 4.4.0 - dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) dotenv: 16.4.7 dotenv-expand: 11.0.7 ejs: 3.1.10 @@ -5566,9 +5566,9 @@ snapshots: dlv@1.1.3: {} - dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): + dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) builder-util: 25.1.7 builder-util-runtime: 9.2.10 fs-extra: 10.1.0 @@ -5646,7 +5646,7 @@ snapshots: electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) archiver: 5.3.2 builder-util: 25.1.7 fs-extra: 10.1.0 @@ -5657,11 +5657,11 @@ snapshots: electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) builder-util: 25.1.7 builder-util-runtime: 9.2.10 chalk: 4.1.2 - dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)) + dmg-builder: 25.1.8(electron-builder-squirrel-windows@25.1.8) fs-extra: 10.1.0 is-ci: 3.0.1 lazy-val: 1.0.5 diff --git a/src/electron/backend/init.ts b/src/electron/backend/init.ts index d7043ca..38ba13e 100644 --- a/src/electron/backend/init.ts +++ b/src/electron/backend/init.ts @@ -1,7 +1,10 @@ import * as path from "path"; -import Database from "better-sqlite3"; +import { Database } from "better-sqlite3"; +import DB from "better-sqlite3"; import { __dirname } from "../dirname.js"; -import { getDatabasePath } from "../utils/backend.js"; +import { getDatabaseDir } from "../utils/backend.js"; +import { migrate } from "./migrate/index.js"; +import { initSchemaInV2 } from "./migrate/migrateToV2"; function getLibSimpleExtensionPath() { switch (process.platform) { @@ -16,13 +19,12 @@ function getLibSimpleExtensionPath() { } } -export function initDatabase() { - const dbPath = getDatabasePath(); - const db = new Database(dbPath, { verbose: console.log }); - const libSimpleExtensionPath = getLibSimpleExtensionPath(); - - db.loadExtension(libSimpleExtensionPath); +function databaseInitialized(db: Database) { + return db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='frame';`).get() + !== undefined; +} +function init(db: Database) { db.exec(` CREATE TABLE IF NOT EXISTS frame ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -95,5 +97,22 @@ export function initDatabase() { END; `); + initSchemaInV2(db); +} + +export function initDatabase() { + const dbPath = getDatabaseDir(); + const db = new DB(dbPath, { verbose: console.log }); + const libSimpleExtensionPath = getLibSimpleExtensionPath(); + + db.loadExtension(libSimpleExtensionPath); + + if (!databaseInitialized(db)) { + init(db); + } + else { + migrate(db); + } + return db; } diff --git a/src/electron/backend/migrate/index.ts b/src/electron/backend/migrate/index.ts new file mode 100644 index 0000000..dab9024 --- /dev/null +++ b/src/electron/backend/migrate/index.ts @@ -0,0 +1,11 @@ +import { Database } from "better-sqlite3"; +import { migrateToV2 } from "./migrateToV2.js"; + +export function migrate(db: Database) { + const configTableExists = + db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='config';`).get() + !== undefined; + if (!configTableExists) { + migrateToV2(db); + } +} \ No newline at end of file diff --git a/src/electron/backend/migrate/migrateToV2.ts b/src/electron/backend/migrate/migrateToV2.ts new file mode 100644 index 0000000..e9157c1 --- /dev/null +++ b/src/electron/backend/migrate/migrateToV2.ts @@ -0,0 +1,81 @@ +import { Database } from "better-sqlite3"; + +interface OldFrame { + id: number; + createAt: string; + imgFilename: string; + segmentID: number | null; + videoPath: string | null; + videoFrameIndex: number | null; + collectionID: number | null; + encoded: number; +} + +export function initSchemaInV2(db: Database) { + db.exec(` + CREATE TABLE config ( + key TEXT PRIMARY KEY, + value TEXT + ); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS encoding_task ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status INT DEFAULT 0 + ); + `); + + db.exec(` + CREATE TABLE IF NOT EXISTS encoding_task_data ( + encodingTaskID INTEGER, + frame ID INTEGER PRIMARY KEY, + FOREIGN KEY (encodingTaskID) REFERENCES encoding_task(id), + FOREIGN KEY (frame) REFERENCES frame(id) + ); + `); + + db.exec(` + CREATE TRIGGER IF NOT EXISTS delete_encoding_task + AFTER UPDATE OF status + ON encoding_task + BEGIN + DELETE FROM encoding_task_data + WHERE encodingTaskID = OLD.id AND NEW.status = 2; + + DELETE FROM encoding_task + WHERE id = OLD.id AND NEW.status = 2; + END; + `); + + db.exec(` + INSERT INTO config (key, value) VALUES ('version', '2'); + `); +} + +/* +* This function assumes that the database does not contain the "config" table, +* and thus needs to be migrated to Version 2. +* */ +export function migrateToV2(db: Database) { + initSchemaInV2(db); + + // Oh we saved tens of millions of bytes for user! + // Before: /Users/username/Library/Application Support/OpenRewind/Record Data/temp/screenshots/1733568609960.jpg + // After: 1733568609960.jpg + const rows = db.prepare("SELECT id, imgFilename FROM frame").all() as OldFrame[]; + rows.forEach(row => { + const filename = row.imgFilename.match(/[^\\/]+$/)?.[0]; + + if (filename) { + db.prepare("UPDATE frame SET imgFilename = ? WHERE id = ?") + .run(filename, row.id); + } + }); + + db.exec(` + ALTER TABLE frame ADD encodeStatus INT DEFAULT 0; + UPDATE frame SET encodeStatus = CASE WHEN encoded THEN 2 ELSE 0 END; + `); +} \ No newline at end of file diff --git a/src/electron/backend/screenshot.ts b/src/electron/backend/screenshot.ts index 394a19c..b7127d6 100644 --- a/src/electron/backend/screenshot.ts +++ b/src/electron/backend/screenshot.ts @@ -1,5 +1,5 @@ import screenshot from "screenshot-desktop"; -import { getScreenshotsPath } from "../utils/backend.js"; +import { getScreenshotsDir } from "../utils/backend.js"; import { join } from "path"; import { Database }from "better-sqlite3"; import SqlString from "sqlstring"; @@ -7,12 +7,13 @@ import SqlString from "sqlstring"; export function startScreenshotLoop(db: Database) { return setInterval(() => { const timestamp = new Date().getTime(); - const screenshotPath = getScreenshotsPath(); - const filename = join(screenshotPath, `${timestamp}.png`); - screenshot({filename: filename}).then((absolutePath) => { + const screenshotDir = getScreenshotsDir(); + const filename = `${timestamp}.png`; + const screenshotPath = join(screenshotDir, filename); + screenshot({filename: screenshotPath, format: "png"}).then((absolutePath) => { const SQL = SqlString.format( "INSERT INTO frame (imgFilename) VALUES (?)", - [absolutePath] + [filename] ); db.exec(SQL); }).catch((err) => { diff --git a/src/electron/index.ts b/src/electron/index.ts index 1cf1081..6a1840e 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -82,6 +82,11 @@ app.on("ready", () => { mainWindow.hide(); }); }); + +app.on("will-quit", ()=> { + dbConnection?.close(); +}); + // app.on("window-all-closed", () => { // if (process.platform !== "darwin") app.quit(); // }); diff --git a/src/electron/utils/backend.ts b/src/electron/utils/backend.ts index 6718d55..8033132 100644 --- a/src/electron/utils/backend.ts +++ b/src/electron/utils/backend.ts @@ -31,12 +31,12 @@ export function createTempDir() { return tempDir; } -export function getDatabasePath() { +export function getDatabaseDir() { const dataDir = createDataDir(); return path.join(dataDir, "main.db"); } -export function getScreenshotsPath() { +export function getScreenshotsDir() { const tempDir = createTempDir(); const screenshotsDir = path.join(tempDir, "screenshots"); if (!fs.existsSync(screenshotsDir)) { @@ -45,7 +45,7 @@ export function getScreenshotsPath() { return screenshotsDir; } -export function getRecordingsPath() { +export function getRecordingsDir() { const dataDir = createDataDir(); return path.join(dataDir, "recordings"); } \ No newline at end of file