Implement folder download as zip in publicFileDownloadHandler
This commit is contained in:
@@ -423,7 +423,10 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
|
|||||||
right: 8,
|
right: 8,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _logout,
|
onPressed: _logout,
|
||||||
icon: Icon(Icons.logout, color: AppTheme.errorColor),
|
icon: Icon(
|
||||||
|
Icons.power_settings_new_rounded,
|
||||||
|
color: AppTheme.errorColor,
|
||||||
|
),
|
||||||
tooltip: 'Logout',
|
tooltip: 'Logout',
|
||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
splashColor: Colors.transparent,
|
splashColor: Colors.transparent,
|
||||||
|
|||||||
@@ -3186,9 +3186,111 @@ func publicFileDownloadHandler(w http.ResponseWriter, r *http.Request, db *datab
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a folder - cannot download folders
|
// Check if it's a folder - if so, create a zip download
|
||||||
if file.Type == "folder" {
|
if file.Type == "folder" {
|
||||||
errors.WriteError(w, errors.CodeInvalidArgument, "Cannot download folders", http.StatusBadRequest)
|
// Get all files under this folder path
|
||||||
|
var folderFiles []database.File
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if link.OrgID != nil {
|
||||||
|
// Org folder - need user ID from context or file owner
|
||||||
|
folderFiles, err = db.GetAllOrgFilesUnderPath(r.Context(), *link.OrgID, *file.UserID, file.Path)
|
||||||
|
} else {
|
||||||
|
// User folder
|
||||||
|
folderFiles, err = db.GetAllUserFilesUnderPath(r.Context(), *file.UserID, file.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get folder contents")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out sub-folders (only include files)
|
||||||
|
var filesToZip []database.File
|
||||||
|
for _, f := range folderFiles {
|
||||||
|
if f.Type == "file" {
|
||||||
|
filesToZip = append(filesToZip, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filesToZip) == 0 {
|
||||||
|
errors.WriteError(w, errors.CodeInvalidArgument, "Folder is empty", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip file in memory
|
||||||
|
var zipBuffer bytes.Buffer
|
||||||
|
zipWriter := zip.NewWriter(&zipBuffer)
|
||||||
|
|
||||||
|
// Get WebDAV client for the file's owner
|
||||||
|
client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get WebDAV client")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each file to the zip
|
||||||
|
for _, fileToZip := range filesToZip {
|
||||||
|
// Download file from storage
|
||||||
|
downloadCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
resp, err := client.Download(downloadCtx, fileToZip.Path, "")
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, fmt.Sprintf("Failed to download file %s for zip", fileToZip.Path))
|
||||||
|
continue // Skip this file, continue with others
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
fileData, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, fmt.Sprintf("Failed to read file %s for zip", fileToZip.Path))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip entry - use relative path within the folder
|
||||||
|
relativePath := strings.TrimPrefix(fileToZip.Path, file.Path)
|
||||||
|
if strings.HasPrefix(relativePath, "/") {
|
||||||
|
relativePath = strings.TrimPrefix(relativePath, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile, err := zipWriter.Create(relativePath)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, fmt.Sprintf("Failed to create zip entry for %s", relativePath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file data to zip
|
||||||
|
_, err = zipFile.Write(fileData)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, fmt.Sprintf("Failed to write file %s to zip", relativePath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close zip writer
|
||||||
|
err = zipWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to close zip writer")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CORS headers for public access
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Range")
|
||||||
|
|
||||||
|
// Set headers for zip download
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
zipFilename := fmt.Sprintf("%s.zip", file.Name)
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", zipFilename))
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", zipBuffer.Len()))
|
||||||
|
|
||||||
|
// Stream the zip file
|
||||||
|
w.Write(zipBuffer.Bytes())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user