Skip to content

Commit a848253

Browse files
committed
refactor: move local storage route in its own router
1 parent bc4d13c commit a848253

File tree

6 files changed

+122
-59
lines changed

6 files changed

+122
-59
lines changed

extra/Caddyfile

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Caddyfile for Vicinae Extension Store
2+
# Caddy automatically provisions and renews SSL certificates via Let's Encrypt
3+
4+
{$DOMAIN} {
5+
# Enable compression
6+
encode gzip zstd
7+
8+
# Reverse proxy to Hono app
9+
reverse_proxy app:3000 {
10+
# Health check
11+
health_uri /
12+
health_interval 30s
13+
health_timeout 5s
14+
15+
# Headers
16+
header_up X-Real-IP {remote_host}
17+
header_up X-Forwarded-For {remote_host}
18+
header_up X-Forwarded-Proto {scheme}
19+
}
20+
21+
# Security headers
22+
header {
23+
# Remove server header
24+
-Server
25+
26+
# Security headers
27+
X-Content-Type-Options "nosniff"
28+
X-Frame-Options "DENY"
29+
Referrer-Policy "strict-origin-when-cross-origin"
30+
31+
# CORS headers for extension downloads
32+
Access-Control-Allow-Origin "*"
33+
Access-Control-Allow-Methods "GET, POST, OPTIONS"
34+
Access-Control-Allow-Headers "Content-Type, Authorization"
35+
}
36+
37+
# Rate limiting for uploads (100 requests per minute)
38+
rate_limit {
39+
zone upload {
40+
key {remote_host}
41+
events 100
42+
window 1m
43+
}
44+
45+
match {
46+
path /extension/upload
47+
}
48+
}
49+
50+
# Logging
51+
log {
52+
output file /data/access.log {
53+
roll_size 100mb
54+
roll_keep 5
55+
}
56+
format json
57+
}
58+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[Unit]
2-
Description=Vicinae Extension Store API
2+
Description=Vicinae Backend
33
After=network.target
44
Documentation=https://github.com/vicinaehq/store
55

src/index.ts

Lines changed: 8 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,25 @@
11
import { Hono } from 'hono'
2-
import { createStorageFromEnv } from './storage/index.js'
3-
import type { StorageAdapter } from './storage/index.js'
4-
import { LocalStorageAdapter } from './storage/index.js'
2+
import type { AppContext } from './types/app.js';
3+
import { createStorageFromEnv, LocalStorageAdapter, type StorageAdapter } from './storage/index.js'
54
import extensionsRouter from './routes/extensions.js'
6-
import { getMimeType } from './utils/mime.js'
7-
8-
const storage = createStorageFromEnv()
9-
10-
type AppContext = {
11-
Variables: {
12-
storage: StorageAdapter
13-
}
14-
}
5+
import localStorageRouter from './routes/storage.js'
156

167
const app = new Hono<AppContext>()
8+
const storage = createStorageFromEnv();
179

1810
app.use('*', async (c, next) => {
1911
c.set('storage', storage)
2012
await next()
2113
})
2214

2315
app.get('/', (c) => {
24-
return c.json({ message: 'Vicinae Extension Store API TEST' })
16+
return c.json({ message: 'Vicinae Backend' })
2517
})
2618

27-
if (storage instanceof LocalStorageAdapter) {
28-
app.get('/storage/*', async (c) => {
29-
try {
30-
const path = c.req.path.replace('/storage/', '');
31-
const file = await storage.get(path);
32-
33-
const contentType = getMimeType(path);
34-
const filename = path.split('/').pop();
35-
36-
// For images and markdown, use inline display instead of attachment
37-
const isInline = contentType.startsWith('image/') || contentType === 'text/markdown';
38-
39-
// Compute ETag from file buffer for cache validation
40-
const crypto = await import('crypto');
41-
const hash = crypto.createHash('md5').update(file).digest('hex');
42-
const etag = `"${hash}"`;
43-
44-
// Check if client has cached version
45-
const ifNoneMatch = c.req.header('if-none-match');
46-
if (ifNoneMatch === etag) {
47-
return new Response(null, { status: 304 });
48-
}
19+
app.route('/', extensionsRouter)
4920

50-
return new Response(file, {
51-
headers: {
52-
'Content-Type': contentType,
53-
'Content-Disposition': isInline
54-
? `inline; filename="${filename}"`
55-
: `attachment; filename="${filename}"`,
56-
// Cache for 1 year since files are content-addressed (path includes version)
57-
'Cache-Control': 'public, max-age=31536000, immutable',
58-
'ETag': etag,
59-
},
60-
});
61-
} catch (error) {
62-
return c.json({ error: 'File not found' }, 404);
63-
}
64-
});
21+
if (storage instanceof LocalStorageAdapter) {
22+
app.route('/', localStorageRouter);
6523
}
6624

67-
app.route('/', extensionsRouter)
68-
6925
export default app

src/routes/extensions.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ import { fetchGitHubUser, getDisplayName } from '../utils/github.js';
1111
import { getExtensionGitHubUrls, buildAssetUrl } from '../utils/repository.js';
1212
import { parseIcon } from '../utils/icons.js';
1313
import { getMimeType } from '../utils/mime.js';
14-
15-
type AppContext = {
16-
Variables: {
17-
storage: StorageAdapter;
18-
};
19-
};
14+
import type { AppContext } from '../types/app.js';
2015

2116
const app = new Hono<AppContext>();
2217

src/routes/storage.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Hono } from "hono";
2+
import { getMimeType } from "../utils/mime";
3+
import type { AppContext } from '../types/app.js';
4+
5+
const app = new Hono<AppContext>();
6+
7+
app.get('/storage/*', async (c) => {
8+
try {
9+
const storage = c.var.storage;
10+
const path = c.req.path.replace('/storage/', '');
11+
const file = await storage.get(path);
12+
13+
const contentType = getMimeType(path);
14+
const filename = path.split('/').pop();
15+
16+
// For images and markdown, use inline display instead of attachment
17+
const isInline = contentType.startsWith('image/') || contentType === 'text/markdown';
18+
19+
// Compute ETag from file buffer for cache validation
20+
const crypto = await import('crypto');
21+
const hash = crypto.createHash('md5').update(file).digest('hex');
22+
const etag = `"${hash}"`;
23+
24+
// Check if client has cached version
25+
const ifNoneMatch = c.req.header('if-none-match');
26+
if (ifNoneMatch === etag) {
27+
return new Response(null, { status: 304 });
28+
}
29+
30+
return new Response(file, {
31+
headers: {
32+
'Content-Type': contentType,
33+
'Content-Disposition': isInline
34+
? `inline; filename="${filename}"`
35+
: `attachment; filename="${filename}"`,
36+
// Cache for 1 year since files are content-addressed (path includes version)
37+
'Cache-Control': 'public, max-age=31536000, immutable',
38+
'ETag': etag,
39+
},
40+
});
41+
} catch (error) {
42+
return c.json({ error: 'File not found' }, 404);
43+
}
44+
});
45+
46+
47+
export default app;

src/types/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { StorageAdapter } from "../storage"
2+
3+
export type AppContext = {
4+
Variables: {
5+
storage: StorageAdapter
6+
}
7+
}

0 commit comments

Comments
 (0)