fix: handle S3 backend in user export download
http.ServeContent requires io.ReadSeeker which S3 files don't support. Add S3 branch using io.Copy with explicit headers, matching the pattern already used in task attachment downloads. Refs: #2347 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
b065c62007
commit
b0ede53c05
|
|
@ -26,7 +26,7 @@ import (
|
|||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/smithy-go"
|
||||
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
|
|
@ -74,10 +74,19 @@ func (f *s3Fs) Stat(name string) (os.FileInfo, error) {
|
|||
return nil, s3ToPathError("stat", name, err)
|
||||
}
|
||||
|
||||
var size int64
|
||||
if head.ContentLength != nil {
|
||||
size = *head.ContentLength
|
||||
}
|
||||
var modTime time.Time
|
||||
if head.LastModified != nil {
|
||||
modTime = *head.LastModified
|
||||
}
|
||||
|
||||
return &s3FileInfo{
|
||||
name: path.Base(name),
|
||||
size: *head.ContentLength,
|
||||
modTime: *head.LastModified,
|
||||
size: size,
|
||||
modTime: modTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -97,24 +106,21 @@ func (f *s3Fs) Remove(name string) error {
|
|||
|
||||
// Unsupported operations
|
||||
|
||||
func (*s3Fs) Create(string) (afero.File, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3Fs) Mkdir(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) MkdirAll(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) OpenFile(string, int, os.FileMode) (afero.File, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3Fs) RemoveAll(string) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Rename(string, string) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chmod(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chown(string, int, int) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chtimes(string, time.Time, time.Time) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Create(string) (afero.File, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3Fs) Mkdir(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) MkdirAll(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) OpenFile(string, int, os.FileMode) (afero.File, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3Fs) RemoveAll(string) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Rename(string, string) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chmod(string, os.FileMode) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chown(string, int, int) error { return ErrS3NotSupported }
|
||||
func (*s3Fs) Chtimes(string, time.Time, time.Time) error { return ErrS3NotSupported }
|
||||
|
||||
// s3ToPathError converts S3 SDK errors into os-compatible path errors.
|
||||
func s3ToPathError(op, name string, err error) error {
|
||||
var opErr *smithy.OperationError
|
||||
if errors.As(err, &opErr) {
|
||||
// HeadObject on a non-existent key returns a 404
|
||||
if opErr.OperationName == "HeadObject" {
|
||||
return &os.PathError{Op: op, Path: name, Err: os.ErrNotExist}
|
||||
}
|
||||
var respErr *smithyhttp.ResponseError
|
||||
if errors.As(err, &respErr) && respErr.HTTPStatusCode() == 404 {
|
||||
return &os.PathError{Op: op, Path: name, Err: os.ErrNotExist}
|
||||
}
|
||||
return &os.PathError{Op: op, Path: name, Err: err}
|
||||
}
|
||||
|
|
@ -176,15 +182,15 @@ func (f *s3File) closeStream() {
|
|||
|
||||
// Unsupported file operations
|
||||
|
||||
func (*s3File) ReadAt([]byte, int64) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Seek(int64, int) (int64, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Write([]byte) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) WriteAt([]byte, int64) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) WriteString(string) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Truncate(int64) error { return ErrS3NotSupported }
|
||||
func (*s3File) Sync() error { return nil }
|
||||
func (*s3File) Readdir(int) ([]os.FileInfo, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3File) Readdirnames(int) ([]string, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3File) ReadAt([]byte, int64) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Seek(int64, int) (int64, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Write([]byte) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) WriteAt([]byte, int64) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) WriteString(string) (int, error) { return 0, ErrS3NotSupported }
|
||||
func (*s3File) Truncate(int64) error { return ErrS3NotSupported }
|
||||
func (*s3File) Sync() error { return nil }
|
||||
func (*s3File) Readdir(int) ([]os.FileInfo, error) { return nil, ErrS3NotSupported }
|
||||
func (*s3File) Readdirnames(int) ([]string, error) { return nil, ErrS3NotSupported }
|
||||
|
||||
// s3FileInfo implements os.FileInfo for S3 objects.
|
||||
type s3FileInfo struct {
|
||||
|
|
@ -193,9 +199,9 @@ type s3FileInfo struct {
|
|||
modTime time.Time
|
||||
}
|
||||
|
||||
func (fi *s3FileInfo) Name() string { return fi.name }
|
||||
func (fi *s3FileInfo) Size() int64 { return fi.size }
|
||||
func (fi *s3FileInfo) Mode() os.FileMode { return 0664 }
|
||||
func (fi *s3FileInfo) Name() string { return fi.name }
|
||||
func (fi *s3FileInfo) Size() int64 { return fi.size }
|
||||
func (fi *s3FileInfo) Mode() os.FileMode { return 0664 }
|
||||
func (fi *s3FileInfo) ModTime() time.Time { return fi.modTime }
|
||||
func (fi *s3FileInfo) IsDir() bool { return false }
|
||||
func (fi *s3FileInfo) Sys() interface{} { return nil }
|
||||
func (fi *s3FileInfo) IsDir() bool { return false }
|
||||
func (fi *s3FileInfo) Sys() interface{} { return nil }
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
|
|
@ -147,6 +150,15 @@ func DownloadUserDataExport(c *echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if config.FilesType.GetString() == "s3" {
|
||||
c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+exportFile.Name+"\"")
|
||||
c.Response().Header().Set("Content-Type", "application/zip")
|
||||
c.Response().Header().Set("Content-Length", strconv.FormatUint(exportFile.Size, 10))
|
||||
c.Response().Header().Set("Last-Modified", exportFile.Created.UTC().Format(http.TimeFormat))
|
||||
_, err = io.Copy(c.Response(), exportFile.File)
|
||||
return err
|
||||
}
|
||||
|
||||
http.ServeContent(c.Response(), c.Request(), exportFile.Name, exportFile.Created, exportFile.File)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue