Files
s3/weed/command/download.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

136 lines
3.6 KiB
Go

package command
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)
var (
d DownloadOptions
)
type DownloadOptions struct {
master *string
server *string // deprecated, for backward compatibility
dir *string
}
func init() {
cmdDownload.Run = runDownload // break init cycle
d.master = cmdDownload.Flag.String("master", "localhost:9333", "Hanzo S3 master location")
d.server = cmdDownload.Flag.String("server", "", "Hanzo S3 master location (deprecated, use -master instead)")
d.dir = cmdDownload.Flag.String("dir", ".", "Download the whole folder recursively if specified.")
}
var cmdDownload = &Command{
UsageLine: "download -master=localhost:9333 -dir=one_directory fid1 [fid2 fid3 ...]",
Short: "download files by file id",
Long: `download files by file id.
Usually you just need to use curl to lookup the file's volume server, and then download them directly.
This download tool combine the two steps into one.
What's more, if you use "s3 upload -maxMB=..." option to upload a big file divided into chunks, you can
use this tool to download the chunks and merge them automatically.
`,
}
func runDownload(cmd *Command, args []string) bool {
util.LoadSecurityConfiguration()
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
// Backward compatibility: if -server is provided, use it
masterServer := *d.master
if *d.server != "" {
masterServer = *d.server
}
*d.dir = util.ResolvePath(*d.dir)
for _, fid := range args {
if e := downloadToFile(func(_ context.Context) pb.ServerAddress { return pb.ServerAddress(masterServer) }, grpcDialOption, fid, *d.dir); e != nil {
fmt.Println("Download Error: ", fid, e)
}
}
return true
}
func downloadToFile(masterFn operation.GetMasterFn, grpcDialOption grpc.DialOption, fileId, saveDir string) error {
fileUrl, jwt, lookupError := operation.LookupFileId(masterFn, grpcDialOption, fileId)
if lookupError != nil {
return lookupError
}
filename, _, rc, err := util_http.DownloadFile(fileUrl, jwt)
if err != nil {
return err
}
defer util_http.CloseResponse(rc)
if filename == "" {
filename = fileId
}
isFileList := false
if strings.HasSuffix(filename, "-list") {
// old command compatible
isFileList = true
filename = filename[0 : len(filename)-len("-list")]
}
f, err := os.OpenFile(path.Join(saveDir, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
return err
}
defer f.Close()
if isFileList {
content, err := io.ReadAll(rc.Body)
if err != nil {
return err
}
fids := strings.Split(string(content), "\n")
for _, partId := range fids {
var n int
_, part, err := fetchContent(masterFn, grpcDialOption, partId)
if err == nil {
n, err = f.Write(part)
}
if err == nil && n < len(part) {
err = io.ErrShortWrite
}
if err != nil {
return err
}
}
} else {
if _, err = io.Copy(f, rc.Body); err != nil {
return err
}
}
return nil
}
func fetchContent(masterFn operation.GetMasterFn, grpcDialOption grpc.DialOption, fileId string) (filename string, content []byte, e error) {
fileUrl, jwt, lookupError := operation.LookupFileId(masterFn, grpcDialOption, fileId)
if lookupError != nil {
return "", nil, lookupError
}
var rc *http.Response
if filename, _, rc, e = util_http.DownloadFile(fileUrl, jwt); e != nil {
return "", nil, e
}
defer util_http.CloseResponse(rc)
content, e = io.ReadAll(rc.Body)
return
}