Skip to content

Commit f66c7d1

Browse files
committed
fix: unify wrangler version and storage providers
1 parent c4a88cc commit f66c7d1

11 files changed

Lines changed: 206 additions & 90 deletions

File tree

bun.lockb

-2.8 KB
Binary file not shown.

cli/src/tasks/deploy-cf.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ export async function runCloudflareDeploy(target: "all" | "server" | "client" =
180180
max_batch_timeout = 5
181181
`)} >> wrangler.toml`.quiet();
182182

183+
if (r2BucketName) {
184+
await $`echo ${stripIndent(`
185+
[[r2_buckets]]
186+
binding = "R2_BUCKET"
187+
bucket_name = "${r2BucketName}"
188+
preview_bucket_name = "${r2BucketName}"
189+
`)} >> wrangler.toml`.quiet();
190+
}
191+
183192
const migrationVersion = await getMigrationVersion("remote", dbName);
184193
const infoExists = await isInfoExist("remote", dbName);
185194
const files = await readdir("./server/sql", { recursive: false });

cli/src/tasks/setup-dev.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ export async function runSetupDev() {
1616
}
1717

1818
const env = parseEnv(fs.readFileSync(envFile, "utf-8"));
19-
const requiredVars = [
19+
const baseRequiredVars = [
2020
"NAME",
2121
"AVATAR",
22-
"S3_ENDPOINT",
23-
"S3_BUCKET",
2422
"RIN_GITHUB_CLIENT_ID",
2523
"RIN_GITHUB_CLIENT_SECRET",
2624
"JWT_SECRET",
27-
"S3_ACCESS_KEY_ID",
28-
"S3_SECRET_ACCESS_KEY",
2925
];
26+
const storageRequiredVars = env.R2_BUCKET_NAME
27+
? ["S3_ACCESS_HOST"]
28+
: ["S3_ENDPOINT", "S3_BUCKET", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"];
29+
const requiredVars = [...baseRequiredVars, ...storageRequiredVars];
3030

3131
const missingVars = requiredVars.filter((name) => !env[name]);
3232
if (missingVars.length > 0) {
@@ -78,6 +78,14 @@ queue = "${env.AI_SUMMARY_QUEUE_NAME || `${env.WORKER_NAME || "rin-server"}-ai-s
7878
queue = "${env.AI_SUMMARY_QUEUE_NAME || `${env.WORKER_NAME || "rin-server"}-ai-summary`}"
7979
max_batch_size = 1
8080
max_batch_timeout = 5
81+
${env.R2_BUCKET_NAME
82+
? `
83+
84+
[[r2_buckets]]
85+
binding = "R2_BUCKET"
86+
bucket_name = "${env.R2_BUCKET_NAME}"
87+
preview_bucket_name = "${env.R2_BUCKET_NAME}"`
88+
: ""}
8189
`;
8290

8391
fs.writeFileSync(path.join(rootDir, "wrangler.toml"), wranglerContent);
@@ -95,8 +103,9 @@ RSS_ENABLE=${env.RSS_ENABLE || "false"}
95103
`RIN_GITHUB_CLIENT_ID=${env.RIN_GITHUB_CLIENT_ID}
96104
RIN_GITHUB_CLIENT_SECRET=${env.RIN_GITHUB_CLIENT_SECRET}
97105
JWT_SECRET=${env.JWT_SECRET}
98-
S3_ACCESS_KEY_ID=${env.S3_ACCESS_KEY_ID}
106+
${env.R2_BUCKET_NAME ? "" : `S3_ACCESS_KEY_ID=${env.S3_ACCESS_KEY_ID}
99107
S3_SECRET_ACCESS_KEY=${env.S3_SECRET_ACCESS_KEY}
108+
`}
100109
`,
101110
);
102111

client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
"rollup-plugin-visualizer": "^5.12.0",
7575
"typescript": "^5.2.2",
7676
"vite": "^6.0.0",
77-
"wrangler": "^3.101.0",
7877
"autoprefixer": "^10.4.19",
7978
"i18next-parser": "^9.0.0",
8079
"postcss": "^8.4.38",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"release": "bun cli/bin/rin.ts release"
3333
},
3434
"devDependencies": {
35-
"turbo": "^1.13.3"
35+
"turbo": "^1.13.3",
36+
"wrangler": "4.71.0"
3637
}
3738
}

server/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
"module": "index.ts",
55
"type": "module",
66
"scripts": {
7-
"build": "wrangler deploy --dry-run --outdir=dist",
8-
"cf:deploy": "wrangler deploy",
9-
"dev": "bun wrangler dev --port 11498",
10-
"dev:cron": "bun wrangler dev --port 11498 --test-scheduled #see https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers",
11-
"cf-typegen": "wrangler types",
7+
"build": "bun x wrangler deploy --dry-run --outdir=dist",
8+
"cf:deploy": "bun x wrangler deploy",
9+
"dev": "bun x wrangler dev --port 11498",
10+
"dev:cron": "bun x wrangler dev --port 11498 --test-scheduled #see https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers",
11+
"cf-typegen": "bun x wrangler types",
1212
"db:gen": "bun drizzle-kit generate",
1313
"preview": "wrangler pages dev dist --compatibility-date=2023-12-20",
1414
"migrate:visits": "bun run scripts/migrate-visits.ts",
@@ -19,7 +19,6 @@
1919
"devDependencies": {
2020
"@types/bun": "^1.1.2",
2121
"drizzle-kit": "^0.21.2",
22-
"wrangler": "^4.71.0",
2322
"@cloudflare/vitest-pool-workers": "^0.1.0",
2423
"typescript": "^5.0.4",
2524
"strip-indent": "^4.0.0"

server/src/custom-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { QueueTask } from "./queue";
33
declare global {
44
interface Env {
55
AI_TASK_QUEUE?: Queue<QueueTask>;
6+
R2_BUCKET?: R2Bucket;
67
}
78
}
89

server/src/services/__tests__/storage.test.ts

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,25 @@ describe('StorageService', () => {
107107
`);
108108
}
109109

110+
function createAppWithEnv(appEnv: Env, uid?: number) {
111+
const serviceApp = new Hono<{ Bindings: Env; Variables: Variables }>();
112+
serviceApp.use(createMiddleware<{ Bindings: Env; Variables: Variables }>(async (c, next) => {
113+
c.set('db', db);
114+
c.set('cache', new TestCacheImpl());
115+
c.set('serverConfig', new TestCacheImpl());
116+
c.set('clientConfig', new TestCacheImpl());
117+
c.set('jwt', {
118+
sign: async (payload: any) => `mock_token_${payload.id}`,
119+
verify: async (token: string) => token.startsWith('mock_token_') ? { id: 1 } : null,
120+
} as JWTUtils);
121+
c.set('env', appEnv);
122+
c.set('uid', uid);
123+
await next();
124+
}));
125+
serviceApp.route('/', StorageService());
126+
return serviceApp;
127+
}
128+
110129
describe('POST / - Upload file', () => {
111130
it('should require authentication', async () => {
112131
const formData = new FormData();
@@ -122,70 +141,91 @@ describe('StorageService', () => {
122141
expect(res.status).toBeLessThanOrEqual(401);
123142
});
124143

125-
it('should return 500 when S3_ENDPOINT is not defined', async () => {
126-
const envNoS3 = createMockEnv({
144+
it('should upload through R2 binding when configured', async () => {
145+
const putCalls: Array<{ key: string; type: string | undefined }> = [];
146+
const r2Env = createMockEnv({
147+
R2_BUCKET: {
148+
put: async (key: string, value: any, options?: R2PutOptions) => {
149+
putCalls.push({
150+
key,
151+
type: options?.httpMetadata && 'contentType' in options.httpMetadata
152+
? options.httpMetadata.contentType
153+
: undefined,
154+
});
155+
return {
156+
key,
157+
version: '1',
158+
size: value.size || 0,
159+
etag: 'etag',
160+
httpEtag: 'etag',
161+
uploaded: new Date(),
162+
storageClass: 'Standard',
163+
checksums: {} as R2Checksums,
164+
writeHttpMetadata: () => {},
165+
} as unknown as R2Object;
166+
},
167+
} as R2Bucket,
168+
S3_ACCESS_HOST: 'https://images.example.com' as any,
127169
S3_ENDPOINT: '' as any,
170+
S3_BUCKET: '' as any,
171+
S3_ACCESS_KEY_ID: '',
172+
S3_SECRET_ACCESS_KEY: '',
128173
});
129174

130-
const appNoS3 = new Hono<{ Bindings: Env; Variables: Variables }>();
131-
appNoS3.use(createMiddleware<{ Bindings: Env; Variables: Variables }>(async (c, next) => {
132-
c.set('db', db);
133-
c.set('cache', new TestCacheImpl());
134-
c.set('serverConfig', new TestCacheImpl());
135-
c.set('clientConfig', new TestCacheImpl());
136-
c.set('jwt', {
137-
sign: async (payload: any) => `mock_token_${payload.id}`,
138-
verify: async (token: string) => token.startsWith('mock_token_') ? { id: 1 } : null,
139-
} as JWTUtils);
140-
c.set('env', envNoS3);
141-
c.set('uid', undefined);
142-
await next();
143-
}));
144-
appNoS3.route('/', StorageService());
175+
const r2App = createAppWithEnv(r2Env, 1);
176+
const formData = new FormData();
177+
formData.append('key', 'test.txt');
178+
formData.append('file', new File(['test content'], 'test.txt', { type: 'text/plain' }));
179+
180+
const res = await r2App.request('/', {
181+
method: 'POST',
182+
body: formData,
183+
}, r2Env);
184+
185+
expect(res.status).toBe(200);
186+
expect(putCalls).toHaveLength(1);
187+
expect(putCalls[0]?.key).toMatch(/^images\/[a-f0-9]+\.txt$/);
188+
expect(putCalls[0]?.type).toBe('text/plain;charset=utf-8');
189+
const payload = await res.json() as { url: string };
190+
expect(payload.url).toMatch(/^https:\/\/images\.example\.com\/images\/[a-f0-9]+\.txt$/);
191+
});
192+
193+
it('should return 500 when S3_ENDPOINT is not defined without R2 binding', async () => {
194+
const envNoS3 = createMockEnv({
195+
S3_ENDPOINT: '' as any,
196+
});
197+
const appNoS3 = createAppWithEnv(envNoS3, 1);
145198

146199
const formData = new FormData();
200+
formData.append('key', 'test.txt');
147201
formData.append('file', new File(['test content'], 'test.txt', { type: 'text/plain' }));
148202

149203
const res = await appNoS3.request('/', {
150204
method: 'POST',
151-
headers: { 'Authorization': 'Bearer mock_token_1' },
152205
body: formData,
153206
}, envNoS3);
154207

155-
expect(res.status).toBeGreaterThanOrEqual(400);
208+
expect(res.status).toBe(500);
209+
expect(await res.text()).toBe('S3_ENDPOINT is not defined');
156210
});
157211

158-
it('should return error when S3_ACCESS_KEY_ID is not defined', async () => {
212+
it('should return error when S3_ACCESS_KEY_ID is not defined without R2 binding', async () => {
159213
const envNoKey = createMockEnv({
160214
S3_ACCESS_KEY_ID: '',
161215
});
162-
163-
const appNoKey = new Hono<{ Bindings: Env; Variables: Variables }>();
164-
appNoKey.use(createMiddleware<{ Bindings: Env; Variables: Variables }>(async (c, next) => {
165-
c.set('db', db);
166-
c.set('cache', new TestCacheImpl());
167-
c.set('serverConfig', new TestCacheImpl());
168-
c.set('clientConfig', new TestCacheImpl());
169-
c.set('jwt', {
170-
sign: async (payload: any) => `mock_token_${payload.id}`,
171-
verify: async (token: string) => token.startsWith('mock_token_') ? { id: 1 } : null,
172-
} as JWTUtils);
173-
c.set('env', envNoKey);
174-
c.set('uid', undefined);
175-
await next();
176-
}));
177-
appNoKey.route('/', StorageService());
216+
const appNoKey = createAppWithEnv(envNoKey, 1);
178217

179218
const formData = new FormData();
219+
formData.append('key', 'test.txt');
180220
formData.append('file', new File(['test content'], 'test.txt', { type: 'text/plain' }));
181221

182222
const res = await appNoKey.request('/', {
183223
method: 'POST',
184-
headers: { 'Authorization': 'Bearer mock_token_1' },
185224
body: formData,
186225
}, envNoKey);
187226

188-
expect(res.status).toBeGreaterThanOrEqual(400);
227+
expect(res.status).toBe(500);
228+
expect(await res.text()).toBe('S3_ACCESS_KEY_ID is not defined');
189229
});
190230
});
191231
});

server/src/services/config-health.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,15 @@ export async function buildHealthCheckResponse(
145145
);
146146
}
147147

148-
const requiredStorageKeys = [
149-
["S3_ENDPOINT", env.S3_ENDPOINT],
150-
["S3_BUCKET", env.S3_BUCKET],
151-
["S3_ACCESS_KEY_ID", env.S3_ACCESS_KEY_ID],
152-
["S3_SECRET_ACCESS_KEY", env.S3_SECRET_ACCESS_KEY],
153-
] as const;
148+
const usesR2Binding = Boolean(env.R2_BUCKET);
149+
const requiredStorageKeys = usesR2Binding
150+
? ([["S3_ACCESS_HOST", env.S3_ACCESS_HOST]] as const)
151+
: ([
152+
["S3_ENDPOINT", env.S3_ENDPOINT],
153+
["S3_BUCKET", env.S3_BUCKET],
154+
["S3_ACCESS_KEY_ID", env.S3_ACCESS_KEY_ID],
155+
["S3_SECRET_ACCESS_KEY", env.S3_SECRET_ACCESS_KEY],
156+
] as const);
154157
const missingStorageKeys = requiredStorageKeys.filter(([, value]) => !value).map(([key]) => key);
155158
const hasAccessHost = Boolean(env.S3_ACCESS_HOST);
156159

server/src/services/storage.ts

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Hono } from "hono";
22
import type { AppContext } from "../core/hono-types";
3-
import { path_join } from "../utils/path";
4-
import { createS3Client, putObject } from "../utils/s3";
3+
import { putStorageObject } from "../utils/storage";
54

65
function buf2hex(buffer: ArrayBuffer) {
76
return [...new Uint8Array(buffer)]
@@ -21,26 +20,6 @@ export function StorageService(): Hono {
2120
const key = body.key as string;
2221
const file = body.file as File;
2322

24-
const endpoint = env.S3_ENDPOINT;
25-
const bucket = env.S3_BUCKET;
26-
const folder = env.S3_FOLDER || '';
27-
const accessHost = env.S3_ACCESS_HOST || endpoint;
28-
const accessKeyId = env.S3_ACCESS_KEY_ID;
29-
const secretAccessKey = env.S3_SECRET_ACCESS_KEY;
30-
const s3 = createS3Client(env);
31-
32-
if (!endpoint) {
33-
return c.text('S3_ENDPOINT is not defined', 500);
34-
}
35-
if (!accessKeyId) {
36-
return c.text('S3_ACCESS_KEY_ID is not defined', 500);
37-
}
38-
if (!secretAccessKey) {
39-
return c.text('S3_SECRET_ACCESS_KEY is not defined', 500);
40-
}
41-
if (!bucket) {
42-
return c.text('S3_BUCKET is not defined', 500);
43-
}
4423
if (!uid) {
4524
return c.text('Unauthorized', 401);
4625
}
@@ -51,20 +30,15 @@ export function StorageService(): Hono {
5130
await file.arrayBuffer()
5231
);
5332
const hash = buf2hex(hashArray);
54-
const hashkey = path_join(folder, hash + "." + suffix);
33+
const hashkey = `${hash}.${suffix}`;
5534

5635
try {
57-
await putObject(
58-
s3,
59-
env,
60-
hashkey,
61-
file,
62-
file.type
63-
);
64-
return c.json({ url: `${accessHost}/${hashkey}` });
36+
const result = await putStorageObject(env, hashkey, file, file.type);
37+
return c.json({ url: result.url });
6538
} catch (e: any) {
6639
console.error(e.message);
67-
return c.text(e.message, 400);
40+
const status = e.message?.includes('is not defined') ? 500 : 400;
41+
return c.text(e.message, status);
6842
}
6943
});
7044

0 commit comments

Comments
 (0)