feat: directly interact with js File APIs and use go-js-promise helper to avoid JSON memory overhead
Some checks reported errors
continuous-integration/drone/push Build encountered an error

This commit is contained in:
RaviAnand Mohabir 2025-04-25 16:01:49 +02:00
parent 1e283d8946
commit 3e5484e9cd
8 changed files with 157 additions and 109 deletions

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/tiff v1.0.1 // indirect github.com/hhrutter/tiff v1.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/image v0.21.0 // indirect golang.org/x/image v0.21.0 // indirect

2
go.sum
View File

@ -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/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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao=
github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI= github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

228
main.go
View File

@ -4,7 +4,6 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io" "io"
"path/filepath" "path/filepath"
@ -14,81 +13,149 @@ import (
"syscall/js" "syscall/js"
promise "github.com/nlepage/go-js-promise"
pdfcpu "github.com/pdfcpu/pdfcpu/pkg/api" pdfcpu "github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
) )
type Customer struct { type Customer struct{ js.Value }
FirstName string `json:"firstName"`
LastName string `json:"lastName"` func (customer Customer) GetFirstName() string {
return customer.Get("firstName").String()
}
func (customer Customer) GetLastName() string {
return customer.Get("lastName").String()
} }
type Document struct { type Document struct {
ID string `json:"id"` js.Value
Name string `json:"name"` bytes []byte
Blob string `json:"blob"`
} }
type CustomerFile struct { func (document Document) GetID() string {
ID string `json:"id"` return document.Get("id").String()
Documents []CustomerDocument `json:"documents"`
Suffix string `json:"suffix"`
} }
type CustomerDocument struct { func (document Document) GetName() string {
ID string `json:"id"` return document.Get("file").Get("name").String()
SelectedPages []string `json:"selectedPages"`
} }
type CreateArchiveInput struct { func (document *Document) GetBytes() ([]byte, error) {
Customer Customer `json:"customer"` if len(document.bytes) > 0 {
Documents []Document `json:"documents"` return document.bytes, nil
Files []CustomerFile `json:"files"` }
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 { type CustomerFile struct{ js.Value }
ResultArchive *string `json:"resultArchive"`
Error *string `json:"error"` 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 { func createArchive(this js.Value, args []js.Value) any {
var input CreateArchiveInput input := CreateArchiveInput{args[0]}
err := json.Unmarshal([]byte(args[0].String()), &input) p, res, rej := promise.New()
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
}
go func() {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
w := zip.NewWriter(buf) w := zip.NewWriter(buf)
now := time.Now() 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 var fileNames []string
for _, file := range input.Files { for _, file := range input.GetFiles() {
if len(file.Documents) == 0 { if len(file.GetDocuments()) == 0 {
reject.Invoke("At least one document must be provided") rej("At least one document must be provided")
return nil return
} }
var document *Document var document *Document
for _, doc := range input.Documents { for _, doc := range input.GetDocuments() {
if doc.ID == file.Documents[0].ID { if doc.GetID() == file.GetDocuments()[0].GetID() {
document = &doc document = &doc
break break
@ -96,18 +163,18 @@ func createArchive(this js.Value, args []js.Value) any {
} }
if document == nil { 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 i := 1
for slices.Index(fileNames, fileName) != -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++ i++
} }
@ -116,49 +183,49 @@ func createArchive(this js.Value, args []js.Value) any {
f, err := w.Create(fileName) f, err := w.Create(fileName)
if err != nil { 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 { if err != nil {
reject.Invoke(err.Error()) rej("Couldn't get bytes:" + err.Error())
return nil return
} }
if ext != ".pdf" { if ext != ".pdf" {
_, err = f.Write(b) _, err = f.Write(b)
if err != nil { if err != nil {
reject.Invoke(err.Error()) rej("Couldn't write file:" + err.Error())
return nil return
} }
continue continue
} }
if len(file.Documents) == 1 { if len(file.GetDocuments()) == 1 {
if len(file.Documents[0].SelectedPages) > 0 { if len(file.GetDocuments()[0].GetSelectedPages()) > 0 {
rs := bytes.NewReader(b) 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 { if err != nil {
reject.Invoke(err.Error()) rej("Couldn't trim PDF: " + err.Error())
return nil return
} }
} else { } else {
_, err = f.Write(b) _, err = f.Write(b)
if err != nil { 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 var rsc []io.ReadSeeker
for i := range file.Documents { for i := range file.GetDocuments() {
var document *Document var document *Document
for _, doc := range input.Documents { for _, doc := range input.GetDocuments() {
if doc.ID == file.Documents[i].ID { if doc.GetID() == file.GetDocuments()[i].GetID() {
document = &doc document = &doc
break break
@ -179,18 +246,18 @@ func createArchive(this js.Value, args []js.Value) any {
} }
if document == nil { 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 { if i != 0 {
b, err = base64.StdEncoding.DecodeString(document.Blob) b, err = document.GetBytes()
if err != nil { 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) rs = bytes.NewReader(b)
) )
if len(file.Documents[i].SelectedPages) > 0 { if len(file.GetDocuments()[i].GetSelectedPages()) > 0 {
var ( var (
buf []byte buf []byte
res = bytes.NewBuffer(buf) 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 { 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())) 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) pdfcpu.MergeRaw(rsc, f, false, nil)
} }
if err = w.Close(); err != nil { if err := w.Close(); err != nil {
reject.Invoke(err.Error()) 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 return p
})
promiseConstructor := js.Global().Get("Promise")
return promiseConstructor.New(handler)
} }
func init() { func init() {

View File

@ -68,29 +68,17 @@ function FileOrganizer() {
const handleSubmit = async ({ customer, files }: FormValues) => { const handleSubmit = async ({ customer, files }: FormValues) => {
await createArchiveAndDownload(async () => { 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 = { const payload: CreateArchiveInput = {
customer, customer,
documents: [], documents: [],
files: [], files: [],
}; };
f.forEach(({ blob, name, suffix, selectedPages }) => { files.forEach(({ file, suffix, selectedPages }) => {
const id = Math.random().toString(36).replace("0.", "doc_"); const id = Math.random().toString(36).replace("0.", "doc_");
const fileId = Math.random().toString(36).replace("0.", "file_"); const fileId = Math.random().toString(36).replace("0.", "file_");
payload.documents.push({ id, blob, name }); payload.documents.push({ id, file });
payload.files.push({ payload.files.push({
id: fileId, id: fileId,
suffix, suffix,

View File

@ -2,7 +2,6 @@ import {
ActionIcon, ActionIcon,
AppShell, AppShell,
Autocomplete, Autocomplete,
Box,
Burger, Burger,
Button, Button,
Group, Group,
@ -33,7 +32,6 @@ import {
import { import {
createArchiveAndDownload, createArchiveAndDownload,
fileCategories, fileCategories,
getFileDataUrl,
systemMessage, systemMessage,
} from "./utils"; } from "./utils";
import { isNotEmpty, useForm } from "@mantine/form"; import { isNotEmpty, useForm } from "@mantine/form";
@ -235,13 +233,7 @@ function Organizrr() {
}; };
}), }),
})), })),
documents: await Promise.all( documents,
documents.map(async (d) => ({
...d,
name: d.file.name,
blob: await getFileDataUrl(d.file),
}))
),
}; };
}); });
}; };

4
src/global.d.ts vendored
View File

@ -1 +1,3 @@
declare function createArchive(payload: string): Promise<string>; import { CreateArchiveInput } from "./types";
declare function createArchive(payload: CreateArchiveInput): Promise<string>;

View File

@ -5,8 +5,7 @@ export interface Customer {
export interface Document { export interface Document {
id: string; id: string;
name: string; file: File;
blob: string;
} }
export interface CustomerFile { export interface CustomerFile {

View File

@ -21,7 +21,7 @@ export const createArchiveAndDownload = async (
const payload = await getPayload(); const payload = await getPayload();
try { try {
const resultArchive = await createArchive(JSON.stringify(payload)); const resultArchive = await createArchive(payload);
notifications.update({ notifications.update({
id, id,