mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-15 14:22:28 -05:00
Replace the github.com/spf13/afero dependency with a purpose-built FileStorage interface (Open, Write, Stat, Remove, MkdirAll) with three implementations: localStorage (with basePath), s3Storage (with key prefix), and memStorage (for tests). Each implementation owns its base path — callers pass only file IDs. Delete s3fs.go, change File.File from afero.File to io.ReadCloser, and fix duplication flows to buffer content for seeking.
212 lines
6.0 KiB
Go
212 lines
6.0 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package files
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/log"
|
|
"code.vikunja.io/api/pkg/modules/keyvalue"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// This file handles storing and retrieving a file for different backends
|
|
var storage FileStorage
|
|
|
|
func setDefaultLocalConfig() {
|
|
config.FilesBasePath.Set(config.ResolvePath(config.FilesBasePath.GetString()))
|
|
}
|
|
|
|
// initS3FileHandler initializes the S3 file backend
|
|
func initS3FileHandler() error {
|
|
// Get S3 configuration
|
|
endpoint := config.FilesS3Endpoint.GetString()
|
|
bucket := config.FilesS3Bucket.GetString()
|
|
region := config.FilesS3Region.GetString()
|
|
accessKey := config.FilesS3AccessKey.GetString()
|
|
secretKey := config.FilesS3SecretKey.GetString()
|
|
|
|
if endpoint == "" {
|
|
return errors.New("S3 endpoint is not configured. Please set files.s3.endpoint")
|
|
}
|
|
if bucket == "" {
|
|
return errors.New("S3 bucket is not configured. Please set files.s3.bucket")
|
|
}
|
|
if accessKey == "" {
|
|
return errors.New("S3 access key is not configured. Please set files.s3.accesskey")
|
|
}
|
|
if secretKey == "" {
|
|
return errors.New("S3 secret key is not configured. Please set files.s3.secretkey")
|
|
}
|
|
|
|
// Create AWS SDK v2 config
|
|
cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
|
|
awsconfig.WithRegion(region),
|
|
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load AWS config: %w", err)
|
|
}
|
|
|
|
// Create S3 client with custom endpoint and path style options
|
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
o.BaseEndpoint = aws.String(endpoint)
|
|
o.UsePathStyle = config.FilesS3UsePathStyle.GetBool()
|
|
if config.FilesS3DisableSigning.GetBool() {
|
|
o.APIOptions = append(o.APIOptions, v4.SwapComputePayloadSHA256ForUnsignedPayloadMiddleware)
|
|
}
|
|
})
|
|
|
|
storage = newS3Storage(bucket, config.FilesBasePath.GetString(), client)
|
|
|
|
return nil
|
|
}
|
|
|
|
// initLocalFileHandler initializes the local filesystem backend
|
|
func initLocalFileHandler() {
|
|
setDefaultLocalConfig()
|
|
storage = newLocalStorage(config.FilesBasePath.GetString())
|
|
}
|
|
|
|
// InitFileHandler creates a new file handler for the file backend we want to use
|
|
func InitFileHandler() error {
|
|
fileType := config.FilesType.GetString()
|
|
|
|
switch fileType {
|
|
case "s3":
|
|
if err := initS3FileHandler(); err != nil {
|
|
return err
|
|
}
|
|
case "local":
|
|
initLocalFileHandler()
|
|
default:
|
|
return fmt.Errorf("invalid file storage type '%s': must be 'local' or 's3'", fileType)
|
|
}
|
|
|
|
if err := ValidateFileStorage(); err != nil {
|
|
return fmt.Errorf("storage validation failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitTestFileHandler initializes a new memory file system for testing
|
|
func InitTestFileHandler() {
|
|
setDefaultLocalConfig()
|
|
storage = newMemStorage()
|
|
}
|
|
|
|
func initFixtures(t *testing.T) {
|
|
// DB fixtures
|
|
db.LoadAndAssertFixtures(t)
|
|
// File fixtures
|
|
InitTestFileFixtures(t)
|
|
err := config.SetMaxFileSizeMBytesFromString("20MB")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// InitTestFileFixtures initializes file fixtures
|
|
func InitTestFileFixtures(t *testing.T) {
|
|
testfile := &File{ID: 1}
|
|
err := storage.Write(testfile.fileID(), bytes.NewReader([]byte("testfile1")), 9)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// InitTests handles the actual bootstrapping of the test env
|
|
func InitTests() {
|
|
var err error
|
|
x, err = db.CreateTestEngine()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = x.Sync2(GetTables()...)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
err = db.InitTestFixtures("files")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
InitTestFileHandler()
|
|
|
|
keyvalue.InitStorage()
|
|
}
|
|
|
|
// FileStat stats a file. This is an exported function to be able to test this from outside of the package
|
|
func FileStat(file *File) (os.FileInfo, error) {
|
|
return storage.Stat(file.fileID())
|
|
}
|
|
|
|
// ValidateFileStorage checks that the configured file storage is writable
|
|
// by creating and removing a temporary file.
|
|
func ValidateFileStorage() error {
|
|
basePath := config.FilesBasePath.GetString()
|
|
|
|
diag := storageDiagnosticInfo(basePath)
|
|
if diag != "" {
|
|
diag = "\n" + diag
|
|
}
|
|
|
|
// For local filesystem, ensure the base directory exists
|
|
if config.FilesType.GetString() == "local" {
|
|
info, err := os.Stat(basePath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("failed to access file storage directory at %s: %w%s", basePath, err, diag)
|
|
}
|
|
|
|
err = os.MkdirAll(basePath, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file storage directory at %s: %w%s", basePath, err, diag)
|
|
}
|
|
} else if !info.IsDir() {
|
|
return fmt.Errorf("file storage path exists but is not a directory: %s", basePath)
|
|
}
|
|
}
|
|
|
|
filename := fmt.Sprintf(".vikunja-check-%d", time.Now().UnixNano())
|
|
|
|
err := storage.Write(filename, bytes.NewReader([]byte{}), 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create test file: %w%s", err, diag)
|
|
}
|
|
|
|
err = storage.Remove(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove test file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|