| package main |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| ) |
|
|
| var ( |
| dataDir = "/data" |
| binDir = "/app/bin" |
| apiKey = "" |
| port = "7860" |
| ) |
|
|
| func init() { |
| if val, ok := os.LookupEnv("DATA_DIR"); ok { |
| dataDir = val |
| } |
| if val, ok := os.LookupEnv("BIN_DIR"); ok { |
| binDir = val |
| } |
| if val, ok := os.LookupEnv("HF_SAVE_API_KEY"); ok { |
| apiKey = val |
| } else { |
| log.Println("WARNING: HF_SAVE_API_KEY environment variable is not set. Write access will not be authenticated.") |
| } |
| if val, ok := os.LookupEnv("PORT"); ok { |
| port = val |
| } |
|
|
| |
| if err := os.MkdirAll(dataDir, 0755); err != nil { |
| log.Fatalf("Failed to create data directory %s: %v", dataDir, err) |
| } |
| } |
|
|
| func main() { |
| |
| authRequired := func(next http.HandlerFunc) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| if apiKey == "" { |
| next(w, r) |
| return |
| } |
| authHeader := r.Header.Get("Authorization") |
| if !strings.HasPrefix(authHeader, "Bearer ") { |
| http.Error(w, "Unauthorized: Missing or invalid Authorization header", http.StatusUnauthorized) |
| return |
| } |
| token := strings.TrimPrefix(authHeader, "Bearer ") |
| if token != apiKey { |
| http.Error(w, "Unauthorized: Invalid API key", http.StatusUnauthorized) |
| return |
| } |
| next(w, r) |
| } |
| } |
|
|
| http.HandleFunc("/init", handleInit) |
| http.HandleFunc("/bin/", handleServeBinaries) |
| http.HandleFunc("/upload", authRequired(handleUpload)) |
| http.HandleFunc("/list", handleList) |
| http.HandleFunc("/download", handleDownload) |
| http.HandleFunc("/health", handleHealth) |
|
|
| log.Printf("Server starting on port %s...", port) |
| log.Printf("Data directory: %s", dataDir) |
| log.Printf("Binaries directory: %s", binDir) |
| if err := http.ListenAndServe(":"+port, nil); err != nil { |
| log.Fatalf("Server failed: %v", err) |
| } |
| } |
|
|
| func handleHealth(w http.ResponseWriter, r *http.Request) { |
| w.WriteHeader(http.StatusOK) |
| w.Write([]byte("OK")) |
| } |
|
|
| |
| func handleInit(w http.ResponseWriter, r *http.Request) { |
| platform := r.URL.Query().Get("platform") |
| host := r.Host |
| scheme := "https" |
| if r.TLS == nil && !strings.Contains(host, "hf.space") { |
| scheme = "http" |
| } |
| serverURL := fmt.Sprintf("%s://%s", scheme, host) |
|
|
| |
| token := r.URL.Query().Get("token") |
|
|
| w.Header().Set("Content-Type", "text/plain") |
|
|
| switch strings.ToLower(platform) { |
| case "windows": |
| |
| script := fmt.Sprintf(`$ServerUrl = "%s" |
| $Token = "%s" |
| $BinDir = "$HOME\.local\bin" |
| if (!(Test-Path $BinDir)) { New-Item -ItemType Directory -Force -Path $BinDir } |
| $ExePath = "$BinDir\hf-save.exe" |
| Write-Host "Downloading hf-save CLI..." -ForegroundColor Cyan |
| Invoke-WebRequest -Uri "$ServerUrl/bin/windows-amd64" -OutFile $ExePath |
| if (!(($env:Path -split ';') -contains $BinDir)) { |
| [System.Environment]::SetEnvironmentVariable("Path", $env:Path + ";$BinDir", "User") |
| $env:Path += ";$BinDir" |
| Write-Host "Added $BinDir to PATH. You may need to restart your terminal." -ForegroundColor Yellow |
| } |
| $ConfigDir = "$HOME\.config\hf-save" |
| if (!(Test-Path $ConfigDir)) { New-Item -ItemType Directory -Force -Path $ConfigDir } |
| if ($Token -eq "") { |
| $Token = Read-Host "Enter your HF_SAVE_API_KEY (Pre-shared API Key)" |
| } |
| $Config = @{ |
| server_url = $ServerUrl |
| api_key = $Token |
| } | ConvertTo-Json |
| $Config | Out-File -FilePath "$ConfigDir\config.json" -Encoding utf8 |
| Write-Host "Installation successful! Try running: hf-save --help" -ForegroundColor Green |
| `, serverURL, token) |
| w.Write([]byte(script)) |
|
|
| case "mac", "darwin": |
| |
| script := fmt.Sprintf(`#!/bin/bash |
| set -e |
| SERVER_URL="%s" |
| TOKEN="%s" |
| BIN_DIR="$HOME/.local/bin" |
| mkdir -p "$BIN_DIR" |
| # Determine CPU architecture |
| ARCH="amd64" |
| if [[ "$(uname -m)" == "arm64" ]]; then |
| ARCH="arm64" |
| fi |
| echo "Downloading hf-save CLI for macOS ($ARCH)..." |
| curl -fsSL "$SERVER_URL/bin/darwin-$ARCH" -o "$BIN_DIR/hf-save" |
| chmod +x "$BIN_DIR/hf-save" |
| |
| # Add to path helper |
| if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then |
| echo "Adding $BIN_DIR to PATH in shell profile..." |
| if [[ "$SHELL" == */zsh ]]; then |
| echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc" |
| else |
| echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" |
| fi |
| export PATH="$BIN_DIR:$PATH" |
| fi |
| |
| CONFIG_DIR="$HOME/.config/hf-save" |
| mkdir -p "$CONFIG_DIR" |
| if [ -z "$TOKEN" ]; then |
| read -p "Enter your HF_SAVE_API_KEY (Pre-shared API Key): " TOKEN |
| fi |
| cat << EOF > "$CONFIG_DIR/config.json" |
| { |
| "server_url": "$SERVER_URL", |
| "api_key": "$TOKEN" |
| } |
| EOF |
| echo "Installation successful! Try running: hf-save --help (Or reload your shell profile)" |
| `, serverURL, token) |
| w.Write([]byte(script)) |
|
|
| default: |
| script := fmt.Sprintf(`#!/bin/bash |
| set -e |
| SERVER_URL="%s" |
| TOKEN="%s" |
| BIN_DIR="$HOME/.local/bin" |
| mkdir -p "$BIN_DIR" |
| # Determine CPU architecture |
| ARCH="amd64" |
| if [[ "$(uname -m)" == "aarch64" ]]; then |
| ARCH="arm64" |
| fi |
| echo "Downloading hf-save CLI for Linux ($ARCH)..." |
| curl -fsSL "$SERVER_URL/bin/linux-$ARCH" -o "$BIN_DIR/hf-save" |
| chmod +x "$BIN_DIR/hf-save" |
| |
| # Add to path helper |
| if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then |
| echo "Adding $BIN_DIR to PATH in shell profile..." |
| echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc" |
| export PATH="$BIN_DIR:$PATH" |
| fi |
| |
| CONFIG_DIR="$HOME/.config/hf-save" |
| mkdir -p "$CONFIG_DIR" |
| if [ -z "$TOKEN" ]; then |
| read -p "Enter your HF_SAVE_API_KEY (Pre-shared API Key): " TOKEN |
| fi |
| cat << EOF > "$CONFIG_DIR/config.json" |
| { |
| "server_url": "$SERVER_URL", |
| "api_key": "$TOKEN" |
| } |
| EOF |
| echo "Installation successful! Try running: hf-save --help (Or run: export PATH=\$HOME/.local/bin:\$PATH)" |
| `, serverURL, token) |
| w.Write([]byte(script)) |
| } |
| } |
|
|
| |
| func handleServeBinaries(w http.ResponseWriter, r *http.Request) { |
| filename := strings.TrimPrefix(r.URL.Path, "/bin/") |
| if filename == "" { |
| http.Error(w, "Not Found", http.StatusNotFound) |
| return |
| } |
|
|
| |
| filename = filepath.Base(filename) |
| binaryPath := filepath.Join(binDir, filename) |
|
|
| if _, err := os.Stat(binaryPath); os.IsNotExist(err) { |
| |
| if strings.HasSuffix(filename, ".exe") { |
| binaryPath = filepath.Join(binDir, "windows-amd64") |
| } else if strings.Contains(filename, "linux") { |
| binaryPath = filepath.Join(binDir, "linux-amd64") |
| } else if strings.Contains(filename, "mac") || strings.Contains(filename, "darwin") { |
| binaryPath = filepath.Join(binDir, "darwin-amd64") |
| } |
| } |
|
|
| if _, err := os.Stat(binaryPath); os.IsNotExist(err) { |
| http.Error(w, "Binary not found on server", http.StatusNotFound) |
| return |
| } |
|
|
| w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) |
| w.Header().Set("Content-Type", "application/octet-stream") |
| http.ServeFile(w, r, binaryPath) |
| } |
|
|
| |
| func handleUpload(w http.ResponseWriter, r *http.Request) { |
| if r.Method != http.MethodPost { |
| http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) |
| return |
| } |
|
|
| mr, err := r.MultipartReader() |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to read multipart data: %v", err), http.StatusBadRequest) |
| return |
| } |
|
|
| var currentPrefix string |
| fileCount := 0 |
|
|
| for { |
| part, err := mr.NextPart() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Error parsing part: %v", err), http.StatusInternalServerError) |
| return |
| } |
|
|
| formName := part.FormName() |
|
|
| if formName == "prefix" { |
| prefixVal, err := io.ReadAll(part) |
| if err != nil { |
| http.Error(w, "Failed to read prefix", http.StatusBadRequest) |
| return |
| } |
| currentPrefix = string(prefixVal) |
| part.Close() |
| continue |
| } |
|
|
| if formName == "files" { |
| if currentPrefix == "" { |
| |
| currentPrefix = time.Now().Format("2006-01-02/15-04-05_upload") |
| } |
|
|
| |
| cleanPrefix := filepath.Clean(currentPrefix) |
| |
| if strings.Contains(cleanPrefix, "..") || strings.HasPrefix(cleanPrefix, "/") { |
| http.Error(w, "Invalid prefix path", http.StatusBadRequest) |
| return |
| } |
|
|
| |
| relPath := part.FileName() |
| if relPath == "" { |
| part.Close() |
| continue |
| } |
|
|
| |
| cleanRelPath := filepath.Clean(relPath) |
| if strings.Contains(cleanRelPath, "..") || strings.HasPrefix(cleanRelPath, "/") { |
| part.Close() |
| continue |
| } |
|
|
| targetPath := filepath.Join(dataDir, cleanPrefix, cleanRelPath) |
| targetDir := filepath.Dir(targetPath) |
|
|
| if err := os.MkdirAll(targetDir, 0755); err != nil { |
| http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError) |
| return |
| } |
|
|
| dst, err := os.Create(targetPath) |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError) |
| return |
| } |
|
|
| if _, err := io.Copy(dst, part); err != nil { |
| dst.Close() |
| http.Error(w, fmt.Sprintf("Failed to save file: %v", err), http.StatusInternalServerError) |
| return |
| } |
| dst.Close() |
| part.Close() |
| fileCount++ |
| } |
| } |
|
|
| w.Header().Set("Content-Type", "application/json") |
| json.NewEncoder(w).Encode(map[string]interface{}{ |
| "success": true, |
| "file_count": fileCount, |
| "prefix": currentPrefix, |
| }) |
| } |
|
|
| |
| func handleList(w http.ResponseWriter, r *http.Request) { |
| dateParam := r.URL.Query().Get("date") |
|
|
| var results []string |
|
|
| if dateParam == "" { |
| |
| entries, err := os.ReadDir(dataDir) |
| if err != nil { |
| http.Error(w, fmt.Sprintf("Failed to read data directory: %v", err), http.StatusInternalServerError) |
| return |
| } |
| for _, entry := range entries { |
| if entry.IsDir() { |
| |
| name := entry.Name() |
| if len(name) == 10 && name[4] == '-' && name[7] == '-' { |
| results = append(results, name) |
| } |
| } |
| } |
| } else { |
| |
| cleanDate := filepath.Clean(dateParam) |
| if strings.Contains(cleanDate, "..") || strings.HasPrefix(cleanDate, "/") { |
| http.Error(w, "Invalid date format", http.StatusBadRequest) |
| return |
| } |
|
|
| targetPath := filepath.Join(dataDir, cleanDate) |
| entries, err := os.ReadDir(targetPath) |
| if err != nil { |
| if os.IsNotExist(err) { |
| w.Header().Set("Content-Type", "application/json") |
| json.NewEncoder(w).Encode([]string{}) |
| return |
| } |
| http.Error(w, fmt.Sprintf("Failed to read date directory: %v", err), http.StatusInternalServerError) |
| return |
| } |
|
|
| for _, entry := range entries { |
| if entry.IsDir() { |
| results = append(results, entry.Name()) |
| } |
| } |
| } |
|
|
| w.Header().Set("Content-Type", "application/json") |
| json.NewEncoder(w).Encode(results) |
| } |
|
|
| |
| func handleDownload(w http.ResponseWriter, r *http.Request) { |
| filePath := r.URL.Query().Get("file") |
| snapshotPath := r.URL.Query().Get("snapshot") |
|
|
| if filePath != "" { |
| |
| cleanPath := filepath.Clean(filePath) |
| if strings.Contains(cleanPath, "..") || strings.HasPrefix(cleanPath, "/") { |
| http.Error(w, "Invalid file path", http.StatusBadRequest) |
| return |
| } |
|
|
| fullPath := filepath.Join(dataDir, cleanPath) |
| if _, err := os.Stat(fullPath); os.IsNotExist(err) { |
| http.Error(w, "File not found", http.StatusNotFound) |
| return |
| } |
|
|
| w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fullPath))) |
| w.Header().Set("Content-Type", "application/octet-stream") |
| http.ServeFile(w, r, fullPath) |
| return |
| } |
|
|
| if snapshotPath != "" { |
| |
| cleanSnapshot := filepath.Clean(snapshotPath) |
| if strings.Contains(cleanSnapshot, "..") || strings.HasPrefix(cleanSnapshot, "/") { |
| http.Error(w, "Invalid snapshot path", http.StatusBadRequest) |
| return |
| } |
|
|
| fullDir := filepath.Join(dataDir, cleanSnapshot) |
| var files []string |
|
|
| err := filepath.Walk(fullDir, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if !info.IsDir() { |
| rel, err := filepath.Rel(fullDir, path) |
| if err == nil { |
| |
| files = append(files, filepath.ToSlash(rel)) |
| } |
| } |
| return nil |
| }) |
|
|
| if err != nil { |
| if os.IsNotExist(err) { |
| http.Error(w, "Snapshot not found", http.StatusNotFound) |
| return |
| } |
| http.Error(w, fmt.Sprintf("Failed to scan snapshot: %v", err), http.StatusInternalServerError) |
| return |
| } |
|
|
| w.Header().Set("Content-Type", "application/json") |
| json.NewEncoder(w).Encode(files) |
| return |
| } |
|
|
| http.Error(w, "Missing query parameter 'file' or 'snapshot'", http.StatusBadRequest) |
| } |
|
|