11import { dateToUnix , WorkerError } from "../common.js"
2+ import { parseSize } from "../shared.js"
23
4+ export type PasteLocation = "KV" | "R2"
5+
6+ // TODO: allow admin to upload permanent paste
7+ // TODO: add filename length check
38export type PasteMetadata = {
4- schemaVersion : number
9+ schemaVersion : 1
10+ location : PasteLocation // new field on V1
511 passwd : string
612
713 lastModifiedAtUnix : number
814 createdAtUnix : number
9- // TODO: allow admin to upload permanent paste
1015 willExpireAtUnix : number
1116
1217 accessCounter : number // a counter representing how frequent it is accessed, to administration usage
18+ sizeBytes : number
19+ filename ?: string
20+ }
21+
22+ type PasteMetadataInStorage = {
23+ schemaVersion : number
24+ location ?: PasteLocation
25+ passwd : string
26+
27+ lastModifiedAtUnix : number
28+ createdAtUnix : number
29+ willExpireAtUnix : number
30+
31+ accessCounter ?: number
1332 sizeBytes ?: number
14- // TODO: add filename length check
1533 filename ?: string
1634}
1735
36+ function migratePasteMetadata ( original : PasteMetadataInStorage ) : PasteMetadata {
37+ return {
38+ schemaVersion : 1 ,
39+ location : original . location || "KV" ,
40+ passwd : original . passwd ,
41+
42+ lastModifiedAtUnix : original . lastModifiedAtUnix ,
43+ createdAtUnix : original . createdAtUnix ,
44+ willExpireAtUnix : original . willExpireAtUnix ,
45+
46+ accessCounter : original . accessCounter || 0 ,
47+ sizeBytes : original . sizeBytes || 0 ,
48+ filename : original . filename ,
49+ }
50+ }
51+
1852export type PasteWithMetadata = {
19- paste : ArrayBuffer
53+ paste : ArrayBuffer | ReadableStream
2054 metadata : PasteMetadata
2155}
2256
23- export async function getPaste ( env : Env , short : string ) : Promise < PasteWithMetadata | null > {
24- const item = await env . PB . getWithMetadata < PasteMetadata > ( short , {
57+ async function updateAccessCounter ( env : Env , short : string , value : ArrayBuffer , metadata : PasteMetadata ) {
58+ // update counter with probability 1%
59+ if ( Math . random ( ) < 0.01 ) {
60+ metadata . accessCounter += 1
61+ try {
62+ await env . PB . put ( short , value , {
63+ metadata : metadata ,
64+ expiration : metadata . willExpireAtUnix ,
65+ } )
66+ } catch ( e ) {
67+ // ignore rate limit message
68+ if ( ! ( e as Error ) . message . includes ( "KV PUT failed: 429 Too Many Requests" ) ) {
69+ throw e
70+ }
71+ }
72+ }
73+ }
74+
75+ export async function getPaste ( env : Env , short : string , ctx : ExecutionContext ) : Promise < PasteWithMetadata | null > {
76+ const item = await env . PB . getWithMetadata < PasteMetadataInStorage > ( short , {
2577 type : "arrayBuffer" ,
2678 } )
2779
@@ -30,34 +82,38 @@ export async function getPaste(env: Env, short: string): Promise<PasteWithMetada
3082 } else if ( item . metadata === null ) {
3183 throw new WorkerError ( 500 , `paste of name '${ short } ' has no metadata` )
3284 } else {
33- if ( item . metadata . willExpireAtUnix < new Date ( ) . getTime ( ) / 1000 ) {
85+ const metadata = migratePasteMetadata ( item . metadata )
86+ const expired = metadata . willExpireAtUnix < new Date ( ) . getTime ( ) / 1000
87+
88+ ctx . waitUntil (
89+ ( async ( ) => {
90+ if ( expired ) {
91+ await deletePaste ( env , short , metadata )
92+ return null
93+ }
94+ await updateAccessCounter ( env , short , item . value ! , metadata )
95+ } ) ( ) ,
96+ )
97+
98+ if ( expired ) {
3499 return null
35100 }
36101
37- // update counter with probability 1%
38- // TODO: use waitUntil API
39- if ( Math . random ( ) < 0.01 ) {
40- item . metadata . accessCounter += 1
41- try {
42- await env . PB . put ( short , item . value , {
43- metadata : item . metadata ,
44- expiration : item . metadata . willExpireAtUnix ,
45- } )
46- } catch ( e ) {
47- // ignore rate limit message
48- if ( ! ( e as Error ) . message . includes ( "KV PUT failed: 429 Too Many Requests" ) ) {
49- throw e
50- }
102+ if ( metadata . location === "R2" ) {
103+ const object = await env . R2 . get ( short )
104+ if ( object === null ) {
105+ throw new WorkerError ( 404 , `cannot find R2 bucket of name '${ short } '` )
51106 }
107+ return { paste : object . body , metadata }
108+ } else {
109+ return { paste : item . value , metadata }
52110 }
53-
54- return { paste : item . value , metadata : item . metadata }
55111 }
56112}
57113
58114// we separate usage of getPasteMetadata and getPaste to make access metric more reliable
59115export async function getPasteMetadata ( env : Env , short : string ) : Promise < PasteMetadata | null > {
60- const item = await env . PB . getWithMetadata < PasteMetadata > ( short , {
116+ const item = await env . PB . getWithMetadata < PasteMetadataInStorage > ( short , {
61117 type : "stream" ,
62118 } )
63119
@@ -69,7 +125,7 @@ export async function getPasteMetadata(env: Env, short: string): Promise<PasteMe
69125 if ( item . metadata . willExpireAtUnix < new Date ( ) . getTime ( ) / 1000 ) {
70126 return null
71127 }
72- return item . metadata
128+ return migratePasteMetadata ( item . metadata )
73129 }
74130}
75131
@@ -87,22 +143,29 @@ export async function updatePaste(
87143 } ,
88144) {
89145 const expirationUnix = dateToUnix ( options . now ) + options . expirationSeconds
90- const putOptions : KVNamespacePutOptions = {
91- metadata : {
92- schemaVersion : 0 ,
93- filename : options . filename || originalMetadata . filename ,
94- passwd : options . passwd ,
95-
96- lastModifiedAtUnix : dateToUnix ( options . now ) ,
97- createdAtUnix : originalMetadata . createdAtUnix ,
98- willExpireAtUnix : expirationUnix ,
99- accessCounter : originalMetadata . accessCounter ,
100- sizeBytes : options . contentLength ,
101- } ,
102- expiration : expirationUnix ,
146+ // since CF does not allow expiration shorter than 60s, extend the expiration to 70s
147+ const expirationUnixSpecified = dateToUnix ( options . now ) + Math . max ( options . expirationSeconds , 70 )
148+
149+ if ( originalMetadata . location === "R2" ) {
150+ await env . R2 . put ( pasteName , content )
151+ }
152+ const metadata : PasteMetadata = {
153+ schemaVersion : 1 ,
154+ location : originalMetadata . location ,
155+ filename : options . filename || originalMetadata . filename ,
156+ passwd : options . passwd ,
157+
158+ lastModifiedAtUnix : dateToUnix ( options . now ) ,
159+ createdAtUnix : originalMetadata . createdAtUnix ,
160+ willExpireAtUnix : expirationUnix ,
161+ accessCounter : originalMetadata . accessCounter ,
162+ sizeBytes : options . contentLength ,
103163 }
104164
105- await env . PB . put ( pasteName , content , putOptions )
165+ await env . PB . put ( pasteName , originalMetadata . location === "R2" ? "" : content , {
166+ metadata : metadata ,
167+ expiration : expirationUnixSpecified ,
168+ } )
106169}
107170
108171export async function createPaste (
@@ -118,22 +181,32 @@ export async function createPaste(
118181 } ,
119182) {
120183 const expirationUnix = dateToUnix ( options . now ) + options . expirationSeconds
121- const putOptions : KVNamespacePutOptions = {
122- metadata : {
123- schemaVersion : 0 ,
124- filename : options . filename ,
125- passwd : options . passwd ,
126-
127- lastModifiedAtUnix : dateToUnix ( options . now ) ,
128- createdAtUnix : dateToUnix ( options . now ) ,
129- willExpireAtUnix : expirationUnix ,
130- accessCounter : 0 ,
131- sizeBytes : options . contentLength ,
132- } ,
133- expiration : expirationUnix ,
184+
185+ // since CF does not allow expiration shorter than 60s, extend the expiration to 70s
186+ const expirationUnixSpecified = dateToUnix ( options . now ) + Math . max ( options . expirationSeconds , 70 )
187+
188+ const location = options . contentLength > parseSize ( env . R2_THRESHOLD ) ! ? "R2" : "KV"
189+ if ( location === "R2" ) {
190+ await env . R2 . put ( pasteName , content )
134191 }
135192
136- await env . PB . put ( pasteName , content , putOptions )
193+ const metadata : PasteMetadata = {
194+ schemaVersion : 1 ,
195+ location : location ,
196+ filename : options . filename ,
197+ passwd : options . passwd ,
198+
199+ lastModifiedAtUnix : dateToUnix ( options . now ) ,
200+ createdAtUnix : dateToUnix ( options . now ) ,
201+ willExpireAtUnix : expirationUnix ,
202+ accessCounter : 0 ,
203+ sizeBytes : options . contentLength ,
204+ }
205+
206+ await env . PB . put ( pasteName , location === "R2" ? "" : content , {
207+ metadata : metadata ,
208+ expiration : expirationUnixSpecified ,
209+ } )
137210}
138211
139212export async function pasteNameAvailable ( env : Env , pasteName : string ) : Promise < boolean > {
@@ -147,6 +220,9 @@ export async function pasteNameAvailable(env: Env, pasteName: string): Promise<b
147220 }
148221}
149222
150- export async function deletePaste ( env : Env , pasteName : string ) : Promise < void > {
223+ export async function deletePaste ( env : Env , pasteName : string , originalMetadata : PasteMetadata ) : Promise < void > {
151224 await env . PB . delete ( pasteName )
225+ if ( originalMetadata . location === "R2" ) {
226+ await env . R2 . delete ( pasteName )
227+ }
152228}
0 commit comments