Skip to content

Commit ad83fdf

Browse files
committed
chore: move database from Firebase to VPS
1 parent fa7fd76 commit ad83fdf

20 files changed

Lines changed: 380 additions & 1682 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ jobs:
3232

3333
- name: Authenticate SteamBrew
3434
run: |
35-
echo "$FIREBASE_CREDENTIALS" | base64 -d > ./apps/www/credentials.json
36-
echo "$BEARER" > ./apps/www/.env
35+
printf '%s\nDB_PATH=/home/ubuntu/net/storage/steambrew/steambrew.db\nPLUGINS_DIR=/home/ubuntu/net/storage/steambrew/plugins\n' "$BEARER" > ./apps/www/.env
3736
env:
38-
FIREBASE_CREDENTIALS: ${{ secrets.FIREBASE_CREDENTIALS }}
3937
BEARER: ${{ secrets.BEARER }}
4038

4139
- name: Install and build docs
@@ -91,7 +89,7 @@ jobs:
9189
- name: Sync www
9290
run: |
9391
rsync -az --delete \
94-
apps/www/.next apps/www/public apps/www/package.json apps/www/.env apps/www/credentials.json \
92+
apps/www/.next apps/www/public apps/www/package.json apps/www/.env \
9593
ubuntu@${{ secrets.SSH_HOST }}:~/net/steambrew/www/
9694
9795
- name: Sync docs

apps/www/bun.lock

Lines changed: 191 additions & 1493 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/www/next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { NextConfig } from 'next';
22

33
const config: NextConfig = {
4+
serverExternalPackages: ['better-sqlite3'],
45
async redirects() {
56
return [
67
{

apps/www/package.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,16 @@
1414
},
1515
"dependencies": {
1616
"@fancyapps/ui": "^5.0.36",
17-
"dotenv": "^16.4.7",
18-
"esbuild": "^0.19.12",
19-
"firebase-admin": "^12.0.0",
20-
"firebase-functions": "^6.3.2",
21-
"firebase-tools": "13",
17+
"better-sqlite3": "^12.10.1",
2218
"hast-util-sanitize": "^5.0.2",
2319
"highlight.js": "^11.11.1",
2420
"next": "^16",
2521
"node-cache": "^5.1.2",
26-
"notyf": "^3.10.0",
2722
"prism-react-renderer": "^2.4.1",
2823
"react": "^18.3.1",
2924
"react-countup": "^6.5.3",
3025
"react-dom": "^18.3.1",
3126
"react-icons": "^5.5.0",
32-
"react-router-dom": "^6.30.0",
3327
"react-select": "^5.10.1",
3428
"react-toastify": "^10.0.6",
3529
"react-tooltip": "^5.28.0",
@@ -45,6 +39,8 @@
4539
},
4640
"devDependencies": {
4741
"@playwright/test": "^1.60.0",
42+
"@types/better-sqlite3": "^7.6.13",
43+
"@types/bun": "^1.3.14",
4844
"@types/node": "22.13.13",
4945
"@types/react": "19.0.12",
5046
"typescript": "5.8.2"

apps/www/src/app/api/Database.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import BetterSqlite3 from 'better-sqlite3';
2+
import { mkdirSync } from 'node:fs';
3+
import { dirname } from 'node:path';
4+
5+
let _db: BetterSqlite3.Database | null = null;
6+
7+
export function getDb(): BetterSqlite3.Database {
8+
if (!_db) {
9+
const path = process.env.DB_PATH;
10+
if (!path) throw new Error('DB_PATH is not set');
11+
mkdirSync(dirname(path), { recursive: true });
12+
_db = new BetterSqlite3(path);
13+
_db.pragma('journal_mode = WAL');
14+
_db.pragma('foreign_keys = ON');
15+
}
16+
return _db;
17+
}
18+
19+
export interface ThemeRow {
20+
id: string;
21+
owner: string;
22+
repo: string;
23+
readme: string | null;
24+
disabled: number;
25+
downloads: number;
26+
created_at: number;
27+
}
28+
29+
export const Themes = {
30+
getAll: (): ThemeRow[] => {
31+
return getDb().prepare('SELECT * FROM themes WHERE disabled = 0').all() as ThemeRow[];
32+
},
33+
getById: (id: string): ThemeRow | null => {
34+
return (getDb().prepare('SELECT * FROM themes WHERE id = ?').get(id) as ThemeRow) ?? null;
35+
},
36+
getByRepo: (owner: string, repo: string): ThemeRow | null => {
37+
return (getDb().prepare('SELECT * FROM themes WHERE owner = ? AND repo = ?').get(owner, repo) as ThemeRow) ?? null;
38+
},
39+
incrementDownload: (id: string): number => {
40+
getDb().prepare('UPDATE themes SET downloads = downloads + 1 WHERE id = ?').run(id);
41+
const row = getDb().prepare('SELECT downloads FROM themes WHERE id = ?').get(id) as { downloads: number } | null;
42+
return row?.downloads ?? 0;
43+
},
44+
};
45+
46+
export interface PluginDownloadRow {
47+
commit_id: string;
48+
download_count: number;
49+
}
50+
51+
export const PluginDownloads = {
52+
getAll: (): PluginDownloadRow[] => {
53+
return getDb().prepare('SELECT * FROM plugin_downloads').all() as PluginDownloadRow[];
54+
},
55+
getCount: (commitId: string): number => {
56+
const row = getDb().prepare('SELECT download_count FROM plugin_downloads WHERE commit_id = ?').get(commitId) as
57+
| { download_count: number }
58+
| null;
59+
return row?.download_count ?? 0;
60+
},
61+
increment: (commitId: string): number => {
62+
getDb()
63+
.prepare(
64+
'INSERT INTO plugin_downloads (commit_id, download_count) VALUES (?, 1) ON CONFLICT(commit_id) DO UPDATE SET download_count = download_count + 1',
65+
)
66+
.run(commitId);
67+
return PluginDownloads.getCount(commitId);
68+
},
69+
};

apps/www/src/app/api/bump/plugin/[slug]/route.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { FieldValue } from 'firebase-admin/firestore';
2-
import { Database } from '../../../Firebase';
1+
import { PluginDownloads } from '../../../Database';
32
import { FetchPlugins } from '../../../v1/plugins/GetPlugins';
43

54
function withCORS(response: Response): Response {
@@ -23,11 +22,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
2322
return withCORS(new Response(JSON.stringify({ success: false, message: 'Plugin not found' }), { status: 404 }));
2423
}
2524

26-
const docRef = Database.collection('downloads').doc(plugin.initCommitId);
27-
await docRef.set({ downloadCount: FieldValue.increment(1) }, { merge: true });
28-
29-
const updatedDoc = await docRef.get();
30-
const newCount = updatedDoc.exists ? updatedDoc.data()?.downloadCount : 1;
25+
const newCount = PluginDownloads.increment(plugin.initCommitId);
3126

3227
return withCORS(
3328
new Response(

apps/www/src/app/api/bump/theme/[slug]/route.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { FieldValue } from 'firebase-admin/firestore';
2-
import { Database } from '../../../Firebase';
1+
import { Themes } from '../../../Database';
32

43
function withCORS(response: Response): Response {
54
response.headers.set('Access-Control-Allow-Origin', 'https://steamloopback.host');
@@ -15,11 +14,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
1514
}
1615

1716
try {
18-
const docRef = Database.collection('V2').doc(slug);
19-
await docRef.set({ download: FieldValue.increment(1) }, { merge: true });
20-
21-
const updatedDoc = await docRef.get();
22-
const newCount = updatedDoc.exists ? updatedDoc.data()?.download : 1;
17+
const newCount = Themes.incrementDownload(slug);
2318

2419
return withCORS(
2520
new Response(

apps/www/src/app/api/v1/plugin/[slug]/route.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { StorageBucket } from '../../../Firebase';
1+
import { join } from 'path';
2+
import { stat } from 'fs/promises';
23
import { FetchPlugins } from '../../plugins/GetPlugins';
34

45
export const dynamic = 'force-dynamic';
@@ -10,13 +11,12 @@ const FindPlugin = async (id: string) => {
1011
throw new Error('Plugin not found');
1112
}
1213

13-
try {
14-
const pluginBuild = StorageBucket.file(`plugins/${plugin.initCommitId}.zip`);
15-
const [exists] = await pluginBuild.exists();
14+
const pluginsDir = process.env.PLUGINS_DIR;
1615

17-
if (exists) {
18-
const [metadata] = await pluginBuild.getMetadata();
19-
plugin.fileSize = Number(metadata.size);
16+
try {
17+
if (pluginsDir) {
18+
const s = await stat(join(pluginsDir, `${plugin.initCommitId}.zip`));
19+
plugin.fileSize = s.size;
2020
plugin.hasValidBuild = true;
2121
} else {
2222
console.warn(`Plugin ${plugin.id} does not have a build available.`);

apps/www/src/app/api/v1/plugins/GetPluginData.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Firebase } from '../../Firebase';
21
import { GithubGraphQL } from '../../v2/GraphQLInterop';
32

43
const FormatSize = (kilobytes) => {

apps/www/src/app/api/v1/plugins/GetPlugins.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { Database, StorageBucket } from '../../Firebase';
2-
import { GetPluginData, PluginDataProps, PluginDataTable } from './GetPluginData';
1+
import { join } from 'path';
2+
import { stat } from 'fs/promises';
3+
import { PluginDownloads } from '../../Database';
4+
import { GetPluginData, PluginDataTable } from './GetPluginData';
35
import { GetPluginMetadata } from './GetPluginMetadata';
46
import { RetrievePluginList } from './GetPluginList';
57

@@ -21,25 +23,32 @@ let cacheTimestamp = 0;
2123
let inflightFetch: Promise<PluginDataTable> | null = null;
2224

2325
const fetchFreshPlugins = async (): Promise<PluginDataTable> => {
24-
console.log('Cache miss — fetching fresh plugin data');
25-
2626
const pluginList = await RetrievePluginList();
2727

28-
const [metadata, pluginData] = await Promise.all([GetPluginMetadata(), GetPluginData(pluginList)]);
29-
const [files, downloadDocs] = await Promise.all([StorageBucket.getFiles({ prefix: 'plugins/' }), Database.collection('downloads').get()]);
28+
const pluginsDir = process.env.PLUGINS_DIR;
29+
if (!pluginsDir) throw new Error('PLUGINS_DIR is not set');
30+
31+
const [metadata, pluginData, downloadRows] = await Promise.all([
32+
GetPluginMetadata(),
33+
GetPluginData(pluginList),
34+
Promise.resolve(PluginDownloads.getAll()),
35+
]);
3036

31-
// Build lookup maps once instead of scanning per-plugin
32-
const fileMetadataMap = new Map<string, any>();
37+
// Build file size map from local disk
38+
const fileMetadataMap = new Map<string, number>();
3339
await Promise.all(
34-
files[0].map(async (file) => {
35-
const [meta] = await file.getMetadata();
36-
fileMetadataMap.set(file.name, meta);
40+
(await import('fs/promises').then(({ readdir }) => readdir(pluginsDir).catch(() => [] as string[]))).map(async (filename) => {
41+
if (!filename.endsWith('.zip')) return;
42+
try {
43+
const s = await stat(join(pluginsDir, filename));
44+
fileMetadataMap.set(`plugins/${filename}`, s.size);
45+
} catch { }
3746
}),
3847
);
3948

4049
const downloadCounts = new Map<string, number>();
41-
downloadDocs.forEach((doc) => {
42-
downloadCounts.set(doc.id, doc.data().downloadCount || 0);
50+
downloadRows.forEach((row) => {
51+
downloadCounts.set(row.commit_id, row.download_count);
4352
});
4453

4554
const metadataByCommit = new Map(metadata.map((m) => [m.commitId, m]));
@@ -53,10 +62,9 @@ const fetchFreshPlugins = async (): Promise<PluginDataTable> => {
5362

5463
const initCommitId = meta.id;
5564
const filePath = `plugins/${initCommitId}.zip`;
56-
const fileMetadata = fileMetadataMap.get(filePath);
57-
58-
if (fileMetadata) {
59-
plugin.downloadSize = FormatBytes(fileMetadata.size);
65+
const fileSize = fileMetadataMap.get(filePath);
66+
if (fileSize !== undefined) {
67+
plugin.downloadSize = FormatBytes(fileSize);
6068
}
6169

6270
plugin.downloadCount = downloadCounts.get(initCommitId) ?? 0;

0 commit comments

Comments
 (0)