From 3e5484e9cd1ccf254c33fc3e83000aac3a645625 Mon Sep 17 00:00:00 2001 From: RaviAnand Mohabir Date: Fri, 25 Apr 2025 16:01:49 +0200 Subject: [PATCH] feat: :sparkles: directly interact with js File APIs and use go-js-promise helper to avoid JSON memory overhead --- go.mod | 1 + go.sum | 2 + main.go | 228 +++++++++++++++++++++++++++--------------- src/FileOrganizer.tsx | 16 +-- src/Organizrr.tsx | 10 +- src/global.d.ts | 4 +- src/types.ts | 3 +- src/utils.tsx | 2 +- 8 files changed, 157 insertions(+), 109 deletions(-) diff --git a/go.mod b/go.mod index 4317067..42bed0b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/tiff v1.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/nlepage/go-js-promise v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/image v0.21.0 // indirect diff --git a/go.sum b/go.sum index c641ca2..94183af 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/nlepage/go-js-promise v1.1.0 h1:BfvywsIMo4cpNOKyoReBWkxEW8f9HMwXqGc45wEKPRs= +github.com/nlepage/go-js-promise v1.1.0/go.mod h1:bdOP0wObXu34euibyK39K1hoBCtlgTKXGc56AGflaRo= github.com/pdfcpu/pdfcpu v0.9.1 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao= github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/main.go b/main.go index b2f448c..d2ba4d8 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "archive/zip" "bytes" "encoding/base64" - "encoding/json" "fmt" "io" "path/filepath" @@ -14,81 +13,149 @@ import ( "syscall/js" + promise "github.com/nlepage/go-js-promise" pdfcpu "github.com/pdfcpu/pdfcpu/pkg/api" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) -type Customer struct { - FirstName string `json:"firstName"` - LastName string `json:"lastName"` +type Customer struct{ js.Value } + +func (customer Customer) GetFirstName() string { + return customer.Get("firstName").String() +} + +func (customer Customer) GetLastName() string { + return customer.Get("lastName").String() } type Document struct { - ID string `json:"id"` - Name string `json:"name"` - Blob string `json:"blob"` + js.Value + bytes []byte } -type CustomerFile struct { - ID string `json:"id"` - Documents []CustomerDocument `json:"documents"` - Suffix string `json:"suffix"` +func (document Document) GetID() string { + return document.Get("id").String() } -type CustomerDocument struct { - ID string `json:"id"` - SelectedPages []string `json:"selectedPages"` +func (document Document) GetName() string { + return document.Get("file").Get("name").String() } -type CreateArchiveInput struct { - Customer Customer `json:"customer"` - Documents []Document `json:"documents"` - Files []CustomerFile `json:"files"` +func (document *Document) GetBytes() ([]byte, error) { + if len(document.bytes) > 0 { + return document.bytes, nil + } + + bytea, err := promise.Await(document.Get("file").Call("arrayBuffer")) + + if err != nil { + return nil, err + } + + uint8Array := js.Global().Get("Uint8Array").New(bytea) + + document.bytes = make([]byte, uint8Array.Length()) + + js.CopyBytesToGo(document.bytes, uint8Array) + + return document.bytes, nil } -type CreateArchiveResult struct { - ResultArchive *string `json:"resultArchive"` - Error *string `json:"error"` +type CustomerFile struct{ js.Value } + +func (file CustomerFile) GetID() string { + return file.Get("id").String() +} + +func (file CustomerFile) GetDocuments() []CustomerDocument { + ds := file.Get("documents") + + cds := make([]CustomerDocument, ds.Length()) + + for i := range ds.Length() { + cds[i] = CustomerDocument{ds.Index(i)} + } + + return cds +} + +func (file CustomerFile) GetSuffix() string { + return file.Get("suffix").String() +} + +type CustomerDocument struct{ js.Value } + +func (document CustomerDocument) GetID() string { + return document.Get("id").String() +} + +func (document CustomerDocument) GetSelectedPages() (sps []string) { + sp := document.Get("selectedPages") + + for i := range sp.Length() { + sps = append(sps, sp.Index(i).String()) + } + + return +} + +type CreateArchiveInput struct{ js.Value } + +func (input CreateArchiveInput) GetCustomer() Customer { + return Customer{input.Get("customer")} +} + +func (input CreateArchiveInput) GetDocuments() []Document { + ds := input.Get("documents") + + documents := make([]Document, ds.Length()) + + for i := range ds.Length() { + documents[i] = Document{ds.Index(i), nil} + } + + return documents +} + +func (input CreateArchiveInput) GetFiles() []CustomerFile { + fs := input.Get("files") + + cfs := make([]CustomerFile, fs.Length()) + + for i := range fs.Length() { + cfs[i] = CustomerFile{fs.Index(i)} + } + + return cfs } func createArchive(this js.Value, args []js.Value) any { - var input CreateArchiveInput + input := CreateArchiveInput{args[0]} - err := json.Unmarshal([]byte(args[0].String()), &input) - - handler := js.FuncOf(func(this js.Value, args []js.Value) any { - var ( - resolve = args[0] - reject = args[1] - ) - - if err != nil { - reject.Invoke(err.Error()) - - return nil - } + p, res, rej := promise.New() + go func() { buf := new(bytes.Buffer) w := zip.NewWriter(buf) now := time.Now() - filePrefix := fmt.Sprintf("%s_%s_%s", now.Format("2006-01-02_15-04-05"), input.Customer.LastName, input.Customer.FirstName) + filePrefix := fmt.Sprintf("%s_%s_%s", now.Format("2006-01-02_15-04-05"), input.GetCustomer().GetLastName(), input.GetCustomer().GetFirstName()) var fileNames []string - for _, file := range input.Files { - if len(file.Documents) == 0 { - reject.Invoke("At least one document must be provided") + for _, file := range input.GetFiles() { + if len(file.GetDocuments()) == 0 { + rej("At least one document must be provided") - return nil + return } var document *Document - for _, doc := range input.Documents { - if doc.ID == file.Documents[0].ID { + for _, doc := range input.GetDocuments() { + if doc.GetID() == file.GetDocuments()[0].GetID() { document = &doc break @@ -96,18 +163,18 @@ func createArchive(this js.Value, args []js.Value) any { } if document == nil { - reject.Invoke("Couldn't find doc by ID: " + file.Documents[0].ID) + rej("Couldn't find doc by ID: " + file.GetDocuments()[0].GetID()) - return nil + return } - ext := strings.ToLower(filepath.Ext(document.Name)) + ext := strings.ToLower(filepath.Ext(document.GetName())) - fileName := fmt.Sprintf("%s_%s%s", filePrefix, file.Suffix, ext) + fileName := fmt.Sprintf("%s_%s%s", filePrefix, file.GetSuffix(), ext) i := 1 for slices.Index(fileNames, fileName) != -1 { - fileName = fmt.Sprintf("%s_%s-%d%s", filePrefix, file.Suffix, i, ext) + fileName = fmt.Sprintf("%s_%s-%d%s", filePrefix, file.GetSuffix(), i, ext) i++ } @@ -116,49 +183,49 @@ func createArchive(this js.Value, args []js.Value) any { f, err := w.Create(fileName) if err != nil { - reject.Invoke(err.Error()) + rej("Couldn't create file: " + err.Error()) - return nil + return } - b, err := base64.StdEncoding.DecodeString(document.Blob) + b, err := document.GetBytes() if err != nil { - reject.Invoke(err.Error()) + rej("Couldn't get bytes:" + err.Error()) - return nil + return } if ext != ".pdf" { _, err = f.Write(b) if err != nil { - reject.Invoke(err.Error()) + rej("Couldn't write file:" + err.Error()) - return nil + return } continue } - if len(file.Documents) == 1 { - if len(file.Documents[0].SelectedPages) > 0 { + if len(file.GetDocuments()) == 1 { + if len(file.GetDocuments()[0].GetSelectedPages()) > 0 { rs := bytes.NewReader(b) - err = pdfcpu.Trim(rs, f, file.Documents[0].SelectedPages, nil) + err = pdfcpu.Trim(rs, f, file.GetDocuments()[0].GetSelectedPages(), nil) if err != nil { - reject.Invoke(err.Error()) + rej("Couldn't trim PDF: " + err.Error()) - return nil + return } } else { _, err = f.Write(b) if err != nil { - reject.Invoke(err.Error()) + rej("219 - Couldn't write file:" + err.Error()) - return nil + return } } @@ -167,11 +234,11 @@ func createArchive(this js.Value, args []js.Value) any { var rsc []io.ReadSeeker - for i := range file.Documents { + for i := range file.GetDocuments() { var document *Document - for _, doc := range input.Documents { - if doc.ID == file.Documents[i].ID { + for _, doc := range input.GetDocuments() { + if doc.GetID() == file.GetDocuments()[i].GetID() { document = &doc break @@ -179,18 +246,18 @@ func createArchive(this js.Value, args []js.Value) any { } if document == nil { - reject.Invoke("Couldn't find doc by ID: " + file.Documents[i].ID) + rej("Couldn't find doc by ID: " + file.GetDocuments()[i].GetID()) - return nil + return } if i != 0 { - b, err = base64.StdEncoding.DecodeString(document.Blob) + b, err = document.GetBytes() if err != nil { - reject.Invoke(err.Error()) + rej("251 - Couldn't get bytes:" + err.Error()) - return nil + return } } @@ -198,18 +265,18 @@ func createArchive(this js.Value, args []js.Value) any { rs = bytes.NewReader(b) ) - if len(file.Documents[i].SelectedPages) > 0 { + if len(file.GetDocuments()[i].GetSelectedPages()) > 0 { var ( buf []byte res = bytes.NewBuffer(buf) ) - err = pdfcpu.Trim(rs, res, file.Documents[i].SelectedPages, nil) + err = pdfcpu.Trim(rs, res, file.GetDocuments()[i].GetSelectedPages(), nil) if err != nil { - reject.Invoke(err.Error()) + rej("270 - Couldn't trim PDF: " + err.Error()) - return nil + return } rsc = append(rsc, bytes.NewReader(res.Bytes())) @@ -221,19 +288,16 @@ func createArchive(this js.Value, args []js.Value) any { pdfcpu.MergeRaw(rsc, f, false, nil) } - if err = w.Close(); err != nil { - reject.Invoke(err.Error()) + if err := w.Close(); err != nil { + rej("Couldn't close ZIP:" + err.Error()) - return nil + return } - resolve.Invoke(base64.StdEncoding.EncodeToString(buf.Bytes())) + res(base64.StdEncoding.EncodeToString(buf.Bytes())) + }() - return nil - }) - - promiseConstructor := js.Global().Get("Promise") - return promiseConstructor.New(handler) + return p } func init() { diff --git a/src/FileOrganizer.tsx b/src/FileOrganizer.tsx index 2bda85b..1576603 100644 --- a/src/FileOrganizer.tsx +++ b/src/FileOrganizer.tsx @@ -68,29 +68,17 @@ function FileOrganizer() { const handleSubmit = async ({ customer, files }: FormValues) => { await createArchiveAndDownload(async () => { - const f = await Promise.all( - files.map(async (f) => { - const dataURL = await getFileDataUrl(f.file); - - return { - ...f, - name: f.file.name, - blob: dataURL, - }; - }) - ); - const payload: CreateArchiveInput = { customer, documents: [], files: [], }; - f.forEach(({ blob, name, suffix, selectedPages }) => { + files.forEach(({ file, suffix, selectedPages }) => { const id = Math.random().toString(36).replace("0.", "doc_"); const fileId = Math.random().toString(36).replace("0.", "file_"); - payload.documents.push({ id, blob, name }); + payload.documents.push({ id, file }); payload.files.push({ id: fileId, suffix, diff --git a/src/Organizrr.tsx b/src/Organizrr.tsx index 979053d..36bf4c8 100644 --- a/src/Organizrr.tsx +++ b/src/Organizrr.tsx @@ -2,7 +2,6 @@ import { ActionIcon, AppShell, Autocomplete, - Box, Burger, Button, Group, @@ -33,7 +32,6 @@ import { import { createArchiveAndDownload, fileCategories, - getFileDataUrl, systemMessage, } from "./utils"; import { isNotEmpty, useForm } from "@mantine/form"; @@ -235,13 +233,7 @@ function Organizrr() { }; }), })), - documents: await Promise.all( - documents.map(async (d) => ({ - ...d, - name: d.file.name, - blob: await getFileDataUrl(d.file), - })) - ), + documents, }; }); }; diff --git a/src/global.d.ts b/src/global.d.ts index 9504d8b..504636a 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1 +1,3 @@ -declare function createArchive(payload: string): Promise; +import { CreateArchiveInput } from "./types"; + +declare function createArchive(payload: CreateArchiveInput): Promise; diff --git a/src/types.ts b/src/types.ts index 266938e..1c9ca0a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,8 +5,7 @@ export interface Customer { export interface Document { id: string; - name: string; - blob: string; + file: File; } export interface CustomerFile { diff --git a/src/utils.tsx b/src/utils.tsx index bfb74b2..6494fe5 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -21,7 +21,7 @@ export const createArchiveAndDownload = async ( const payload = await getPayload(); try { - const resultArchive = await createArchive(JSON.stringify(payload)); + const resultArchive = await createArchive(payload); notifications.update({ id,