package services import ( "archive/tar" "compress/gzip" "context" "fmt" "io" "path" "strings" "time" "fwh-toolbox-server/cache" minioRepo "fwh-toolbox-server/repositories/minio" mongoRepo "fwh-toolbox-server/repositories/mongo" ) type DataDownloadArchiveEntry struct { Path string `json:"path"` Size int64 `json:"size"` Directory bool `json:"directory"` } func ListDataDownloadArchive(ctx context.Context, documentID string) ([]DataDownloadArchiveEntry, error) { lock, err := cache.Lock(ctx, "data-download:content:"+documentID, 10*time.Minute, 100*time.Millisecond, 10*time.Second) if err != nil { return nil, err } defer lock.SafeRelease(context.Background()) reader, compressed, archive, err := openDataDownloadTarGz(ctx, documentID) if err != nil { return nil, err } defer reader.Close() defer compressed.Close() var entries []DataDownloadArchiveEntry for { header, err := archive.Next() if err == io.EOF { break } if err != nil { return nil, err } clean, err := safeArchiveEntry(header.Name) if err != nil { continue } entries = append(entries, DataDownloadArchiveEntry{Path: clean, Size: header.Size, Directory: header.FileInfo().IsDir()}) } return entries, nil } func ReadDataDownloadArchiveEntry(ctx context.Context, documentID, entryPath string) ([]byte, error) { lock, err := cache.Lock(ctx, "data-download:content:"+documentID, 10*time.Minute, 100*time.Millisecond, 10*time.Second) if err != nil { return nil, err } defer lock.SafeRelease(context.Background()) wanted, err := safeArchiveEntry(entryPath) if err != nil { return nil, err } reader, compressed, archive, err := openDataDownloadTarGz(ctx, documentID) if err != nil { return nil, err } defer reader.Close() defer compressed.Close() for { header, err := archive.Next() if err == io.EOF { return nil, fmt.Errorf("archive entry not found") } if err != nil { return nil, err } clean, cleanErr := safeArchiveEntry(header.Name) if cleanErr != nil || clean != wanted || header.FileInfo().IsDir() { continue } return io.ReadAll(io.LimitReader(archive, header.Size)) } } func openDataDownloadTarGz(ctx context.Context, documentID string) (io.ReadCloser, *gzip.Reader, *tar.Reader, error) { document, err := mongoRepo.GetDataDownloadDocument(ctx, documentID) if err != nil { return nil, nil, nil, err } if document.BinaryObjectKey == "" || !(strings.HasSuffix(strings.ToLower(document.BinaryObjectKey), ".tar.gz") || strings.HasSuffix(strings.ToLower(document.BinaryObjectKey), ".tgz")) { return nil, nil, nil, fmt.Errorf("document is not a tar.gz archive") } reader, err := minioRepo.Open(ctx, document.BinaryObjectKey) if err != nil { return nil, nil, nil, err } gz, err := gzip.NewReader(reader) if err != nil { _ = reader.Close() return nil, nil, nil, err } return reader, gz, tar.NewReader(gz), nil } func safeArchiveEntry(value string) (string, error) { clean := strings.TrimPrefix(path.Clean("/"+strings.ReplaceAll(value, "\\", "/")), "/") if clean == "." || clean == "" || strings.HasPrefix(clean, "../") { return "", fmt.Errorf("invalid archive entry") } return clean, nil }