1- import type { ListProducts , ListVariants } from "@lemonsqueezy/lemonsqueezy.js" ;
1+ import {
2+ listFiles ,
3+ type ListProducts ,
4+ type ListVariants ,
5+ } from "@lemonsqueezy/lemonsqueezy.js" ;
26import type { Polar } from "@polar-sh/sdk" ;
37import type {
48 BenefitLicenseKeyExpirationProperties ,
9+ FileRead ,
510 Interval ,
611 Organization ,
12+ Product ,
713 ProductOneTimeCreate ,
814 ProductPriceOneTimeCustomCreate ,
915 ProductPriceOneTimeFixedCreate ,
@@ -13,6 +19,13 @@ import type {
1319 ProductRecurringCreate ,
1420 Timeframe ,
1521} from "@polar-sh/sdk/models/components" ;
22+ import fs from "node:fs" ;
23+ import path from "node:path" ;
24+ import os from "node:os" ;
25+ import mime from "mime-types" ;
26+ import https from "node:https" ;
27+ import { Upload } from "./upload.js" ;
28+ import { uploadFailedMessage , uploadMessage } from "./ui/upload.js" ;
1629
1730const resolveInterval = (
1831 interval : ListVariants [ "data" ] [ number ] [ "attributes" ] [ "interval" ] ,
@@ -168,5 +181,171 @@ export const createProduct = async (
168181 } ) ;
169182 }
170183
184+ try {
185+ await handleFiles ( api , organization , variant , product ) ;
186+ } catch ( e ) {
187+ await uploadFailedMessage ( ) ;
188+ }
189+
171190 return product ;
172191} ;
192+
193+ const handleFiles = async (
194+ api : Polar ,
195+ organization : Organization ,
196+ variant : ListVariants [ "data" ] [ number ] ,
197+ product : Product ,
198+ ) => {
199+ const files = await listFiles ( {
200+ filter : {
201+ variantId : variant . id ,
202+ } ,
203+ } ) ;
204+
205+ // Group files with same variant id and download them
206+ const tempDir = await fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , "polar-" ) ) ;
207+
208+ const groupedFiles =
209+ files . data ?. data ?. reduce <
210+ Record < string , { downloadUrl : string ; filePath : string } [ ] >
211+ > ( ( acc , file ) => {
212+ if ( "attributes" in file && "variant_id" in file . attributes ) {
213+ const filePath = path . join ( tempDir , file . attributes . name ) ;
214+ const url = new URL ( file . attributes . download_url ) ;
215+
216+ acc [ file . attributes . variant_id ] = [
217+ ...( acc [ file . attributes . variant_id ] ?? [ ] ) ,
218+ {
219+ downloadUrl : url . toString ( ) ,
220+ filePath,
221+ } ,
222+ ] ;
223+ }
224+
225+ return acc ;
226+ } , { } ) ?? { } ;
227+
228+ await Promise . all (
229+ Object . values ( groupedFiles )
230+ . flat ( )
231+ . map ( ( file ) => downloadFile ( file . downloadUrl , file . filePath ) ) ,
232+ ) ;
233+
234+ // Create one benefit per variant, upload the files to the benefit, and add the benefit to the product
235+
236+ for ( const [ _ , files ] of Object . entries ( groupedFiles ) ) {
237+ const fileUploads = await Promise . all (
238+ files . map ( ( file ) => uploadFile ( api , organization , file . filePath ) ) ,
239+ ) ;
240+
241+ const benefit = await api . benefits . create ( {
242+ type : "downloadables" ,
243+ description : product . name ,
244+ properties : {
245+ files : fileUploads . map ( ( file ) => file . id ) ,
246+ } ,
247+ organizationId : organization . id ,
248+ } ) ;
249+
250+ await api . products . updateBenefits ( {
251+ id : product . id ,
252+ productBenefitsUpdate : {
253+ benefits : [ benefit . id ] ,
254+ } ,
255+ } ) ;
256+ }
257+
258+ // Clean up temporary files
259+ await Promise . all (
260+ Object . values ( groupedFiles )
261+ . flat ( )
262+ . map ( ( file ) => fs . promises . unlink ( file . filePath ) ) ,
263+ ) ;
264+
265+ await fs . promises . rmdir ( tempDir ) ;
266+ } ;
267+
268+ const downloadFile = ( url : string , filePath : string ) => {
269+ return new Promise < void > ( ( resolve , reject ) => {
270+ const options = {
271+ method : "GET" ,
272+ headers : {
273+ "Content-Type" : "application/octet-stream" ,
274+ } ,
275+ } ;
276+
277+ const writer = fs . createWriteStream ( filePath ) ;
278+
279+ const request = https . get ( url , options , ( response ) => {
280+ if ( response . statusCode !== 200 ) {
281+ fs . unlink ( filePath , ( e ) => {
282+ if ( e ) {
283+ console . error ( e ) ;
284+ }
285+ } ) ;
286+ reject ( response ) ;
287+ return ;
288+ }
289+
290+ response . pipe ( writer ) ;
291+
292+ writer . on ( "finish" , ( ) => {
293+ writer . close ( ) ;
294+ resolve ( ) ;
295+ } ) ;
296+ } ) ;
297+
298+ request . on ( "error" , ( err ) => {
299+ console . error ( err ) ;
300+
301+ fs . unlink ( filePath , ( e ) => {
302+ if ( e ) {
303+ console . error ( e ) ;
304+ }
305+ } ) ;
306+ } ) ;
307+
308+ writer . on ( "error" , ( err ) => {
309+ console . error ( err ) ;
310+
311+ fs . unlink ( filePath , ( e ) => {
312+ if ( e ) {
313+ console . error ( e ) ;
314+ }
315+ } ) ;
316+ } ) ;
317+
318+ request . end ( ) ;
319+ } ) ;
320+ } ;
321+
322+ const uploadFile = async (
323+ api : Polar ,
324+ organization : Organization ,
325+ filePath : string ,
326+ ) => {
327+ const readStream = fs . createReadStream ( filePath ) ;
328+ const mimeType = mime . lookup ( filePath ) || "application/octet-stream" ;
329+
330+ const fileUploadPromise = new Promise < FileRead > ( ( resolve ) => {
331+ const upload = new Upload ( api , {
332+ organization,
333+ file : {
334+ name : path . basename ( filePath ) ,
335+ type : mimeType ,
336+ size : fs . statSync ( filePath ) . size ,
337+ readStream,
338+ } ,
339+ onFileUploadProgress : ( ) => { } ,
340+ onFileUploaded : resolve ,
341+ } ) ;
342+
343+ upload . run ( ) ;
344+ } ) ;
345+
346+ await uploadMessage ( fileUploadPromise ) ;
347+
348+ const fileUpload = await fileUploadPromise ;
349+
350+ return fileUpload ;
351+ } ;
0 commit comments