diff --git a/shortcuts/apps/html_publish_zip.go b/shortcuts/apps/html_publish_zip.go new file mode 100644 index 000000000..cc0424e17 --- /dev/null +++ b/shortcuts/apps/html_publish_zip.go @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package apps + +import ( + "archive/zip" + "bytes" + "crypto/sha256" + "encoding/hex" + "io" + + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/extension/fileio" +) + +// htmlPublishArchive is the in-memory zip ready for TOS upload. +type htmlPublishArchive struct { + Body []byte + Size int64 + SHA256 string +} + +func buildHTMLPublishZip(fio fileio.FileIO, candidates []htmlPublishCandidate) (*htmlPublishArchive, error) { + if len(candidates) == 0 { + return nil, appsValidationParamError("--path", "no files to pack") + } + + var buf bytes.Buffer + hasher := sha256.New() + multi := io.MultiWriter(&buf, hasher) + zw := zip.NewWriter(multi) + + for _, c := range candidates { + if err := writeHTMLPublishZipEntry(fio, zw, c); err != nil { + _ = zw.Close() + return nil, err + } + } + + if err := zw.Close(); err != nil { + return nil, appsFileIOError(err, "zip close: %v", err) + } + + return &htmlPublishArchive{ + Body: buf.Bytes(), + Size: int64(buf.Len()), + SHA256: hex.EncodeToString(hasher.Sum(nil)), + }, nil +} + +func writeHTMLPublishZipEntry(fio fileio.FileIO, zw *zip.Writer, c htmlPublishCandidate) error { + if isUnsafeRelPath(c.RelPath) { + return errs.NewInternalError(errs.SubtypeUnknown, "invalid zip entry name %q", c.RelPath) + } + + src, err := fio.Open(c.AbsPath) + if err != nil { + return appsInputPathEntryError(c.AbsPath, err) + } + defer src.Close() + + w, err := zw.Create(c.RelPath) + if err != nil { + return appsFileIOError(err, "create zip entry %s: %v", c.RelPath, err) + } + if _, err := io.Copy(w, src); err != nil { + return appsFileIOError(err, "copy %s: %v", c.RelPath, err) + } + return nil +} diff --git a/shortcuts/apps/html_publish_zip_test.go b/shortcuts/apps/html_publish_zip_test.go new file mode 100644 index 000000000..b948e285a --- /dev/null +++ b/shortcuts/apps/html_publish_zip_test.go @@ -0,0 +1,175 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package apps + +import ( + "archive/zip" + "bytes" + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestBuildHTMLPublishZip_RoundTrip(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(""), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "style.css"), []byte("body{}"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + fio := newTestFIO() + candidates, err := walkHTMLPublishCandidates(fio, dir) + if err != nil { + t.Fatalf("walk: %v", err) + } + archive, err := buildHTMLPublishZip(fio, candidates) + if err != nil { + t.Fatalf("buildHTMLPublishZip: %v", err) + } + + if len(archive.SHA256) != 64 { + t.Fatalf("SHA256 wrong len: %d", len(archive.SHA256)) + } + if archive.Size <= 0 || int64(len(archive.Body)) != archive.Size { + t.Fatalf("size=%d body=%d", archive.Size, len(archive.Body)) + } + + r, err := zip.NewReader(bytes.NewReader(archive.Body), archive.Size) + if err != nil { + t.Fatalf("read zip: %v", err) + } + names := make(map[string]bool) + for _, f := range r.File { + names[f.Name] = true + } + if !names["index.html"] || !names["style.css"] { + t.Errorf("zip entries = %v, want index.html and style.css", names) + } + + // Verify content round-trip for index.html. + for _, f := range r.File { + if f.Name == "index.html" { + rc, err := f.Open() + if err != nil { + t.Fatalf("open entry: %v", err) + } + body, err := io.ReadAll(rc) + rc.Close() + if err != nil || string(body) != "" { + t.Fatalf("body=%q err=%v", body, err) + } + } + } +} + +func TestBuildHTMLPublishZip_EmptyCandidates(t *testing.T) { + if _, err := buildHTMLPublishZip(newTestFIO(), nil); err == nil { + t.Fatalf("expected error") + } +} + +func TestBuildHTMLPublishZip_SHA256Stable(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("