feat(files): add folder download as zip
- Add zip folder API endpoint - Add download button to folder rows - Enforce max archive size limit - Document endpoint in OpenAPI
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var ErrZipTooLarge = errors.New("folder archive exceeds size limit")
|
||||
|
||||
// ZipDirectory writes dir into a temp .zip file and returns its path.
|
||||
func ZipDirectory(dir string, maxBytes int64) (string, error) {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", errors.New("path is not a directory")
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "win64_mp-zip-*.zip")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := tmp.Name()
|
||||
cleanup := func() { _ = os.Remove(path) }
|
||||
|
||||
zw := zip.NewWriter(tmp)
|
||||
var written int64
|
||||
err = filepath.WalkDir(dir, func(current string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
return nil
|
||||
}
|
||||
if !isWithin(dir, current) {
|
||||
return ErrOutsideRoot
|
||||
}
|
||||
rel, err := filepath.Rel(dir, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if maxBytes > 0 && written+info.Size() > maxBytes {
|
||||
return ErrZipTooLarge
|
||||
}
|
||||
hdr, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hdr.Name = filepath.ToSlash(rel)
|
||||
hdr.Method = zip.Deflate
|
||||
w, err := zw.CreateHeader(hdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Open(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := io.Copy(w, f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
written += n
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
_ = zw.Close()
|
||||
_ = tmp.Close()
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
_ = tmp.Close()
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
cleanup()
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// ZipArchiveName returns a safe download filename for a directory path.
|
||||
func ZipArchiveName(dir string) string {
|
||||
base := filepath.Base(dir)
|
||||
if base == "" || base == "." || base == string(os.PathSeparator) {
|
||||
return "folder.zip"
|
||||
}
|
||||
return fmt.Sprintf("%s.zip", base)
|
||||
}
|
||||
Reference in New Issue
Block a user