2023-07-06 00:01:40 +08:00
package v1
2022-02-03 15:32:03 +08:00
import (
2023-05-26 09:43:51 +08:00
"context"
2022-02-04 16:51:48 +08:00
"encoding/json"
2022-02-03 15:32:03 +08:00
"fmt"
2022-08-20 11:36:24 +08:00
"io"
2022-02-03 15:32:03 +08:00
"net/http"
2022-12-12 20:00:21 +08:00
"net/url"
2023-03-19 19:37:57 +08:00
"os"
"path/filepath"
2023-03-09 22:41:48 +08:00
"regexp"
2022-02-03 15:32:03 +08:00
"strconv"
2023-01-07 10:51:34 +08:00
"strings"
2022-10-29 15:40:09 +08:00
"time"
2022-02-03 15:32:03 +08:00
2023-02-13 19:36:48 +08:00
"github.com/labstack/echo/v4"
2023-01-02 23:18:12 +08:00
"github.com/pkg/errors"
2023-09-17 22:55:13 +08:00
"go.uber.org/zap"
2023-10-26 09:02:50 +08:00
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
2023-02-13 19:36:48 +08:00
"github.com/usememos/memos/plugin/storage/s3"
2023-11-05 21:41:47 +08:00
"github.com/usememos/memos/server/service/metric"
2023-05-26 09:43:51 +08:00
"github.com/usememos/memos/store"
2022-02-03 15:32:03 +08:00
)
2023-07-06 00:01:40 +08:00
type Resource struct {
2023-08-04 21:55:07 +08:00
ID int32 ` json:"id" `
2023-07-06 00:01:40 +08:00
// Standard fields
2023-08-04 21:55:07 +08:00
CreatorID int32 ` json:"creatorId" `
2023-07-06 00:01:40 +08:00
CreatedTs int64 ` json:"createdTs" `
UpdatedTs int64 ` json:"updatedTs" `
// Domain specific fields
Filename string ` json:"filename" `
Blob [ ] byte ` json:"-" `
InternalPath string ` json:"-" `
ExternalLink string ` json:"externalLink" `
Type string ` json:"type" `
Size int64 ` json:"size" `
}
type CreateResourceRequest struct {
2023-09-10 10:33:22 +08:00
Filename string ` json:"filename" `
ExternalLink string ` json:"externalLink" `
Type string ` json:"type" `
2023-07-06 00:01:40 +08:00
}
type FindResourceRequest struct {
2023-08-04 21:55:07 +08:00
ID * int32 ` json:"id" `
CreatorID * int32 ` json:"creatorId" `
2023-07-06 00:01:40 +08:00
Filename * string ` json:"filename" `
}
type UpdateResourceRequest struct {
2023-07-08 11:29:50 +08:00
Filename * string ` json:"filename" `
2023-07-06 00:01:40 +08:00
}
2022-11-18 21:17:52 +08:00
const (
2023-05-13 22:27:28 +08:00
// The upload memory buffer is 32 MiB.
// It should be kept low, so RAM usage doesn't get out of control.
// This is unrelated to maximum upload size limit, which is now set through system setting.
maxUploadBufferSizeBytes = 32 << 20
MebiByte = 1024 * 1024
2022-11-18 21:17:52 +08:00
)
2023-03-09 22:41:48 +08:00
var fileKeyPattern = regexp . MustCompile ( ` \ { [a-z] { 1,9}\} ` )
2023-07-06 00:01:40 +08:00
func ( s * APIV1Service ) registerResourceRoutes ( g * echo . Group ) {
2023-08-09 22:30:27 +08:00
g . GET ( "/resource" , s . GetResourceList )
g . POST ( "/resource" , s . CreateResource )
g . POST ( "/resource/blob" , s . UploadResource )
g . PATCH ( "/resource/:resourceId" , s . UpdateResource )
g . DELETE ( "/resource/:resourceId" , s . DeleteResource )
2023-08-09 21:53:06 +08:00
}
2022-02-03 15:32:03 +08:00
2023-08-09 22:30:27 +08:00
// GetResourceList godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Get a list of resources
// @Tags resource
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []store.Resource "Resource list"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to fetch resource list"
// @Router /api/v1/resource [GET]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) GetResourceList ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
find := & store . FindResource {
CreatorID : & userID ,
}
if limit , err := strconv . Atoi ( c . QueryParam ( "limit" ) ) ; err == nil {
find . Limit = & limit
}
if offset , err := strconv . Atoi ( c . QueryParam ( "offset" ) ) ; err == nil {
find . Offset = & offset
}
list , err := s . Store . ListResources ( ctx , find )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to fetch resource list" ) . SetInternal ( err )
}
resourceMessageList := [ ] * Resource { }
for _ , resource := range list {
resourceMessageList = append ( resourceMessageList , convertResourceFromStore ( resource ) )
}
return c . JSON ( http . StatusOK , resourceMessageList )
}
2023-08-09 22:30:27 +08:00
// CreateResource godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Create resource
// @Tags resource
// @Accept json
// @Produce json
// @Param body body CreateResourceRequest true "Request object."
// @Success 200 {object} store.Resource "Created resource"
// @Failure 400 {object} nil "Malformatted post resource request | Invalid external link | Invalid external link scheme | Failed to request %s | Failed to read %s | Failed to read mime from %s"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to save resource | Failed to create resource | Failed to create activity"
// @Router /api/v1/resource [POST]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) CreateResource ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
request := & CreateResourceRequest { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( request ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted post resource request" ) . SetInternal ( err )
}
create := & store . Resource {
CreatorID : userID ,
Filename : request . Filename ,
ExternalLink : request . ExternalLink ,
Type : request . Type ,
}
if request . ExternalLink != "" {
// Only allow those external links scheme with http/https
linkURL , err := url . Parse ( request . ExternalLink )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Invalid external link" ) . SetInternal ( err )
2023-07-06 00:01:40 +08:00
}
2023-08-09 21:53:06 +08:00
if linkURL . Scheme != "http" && linkURL . Scheme != "https" {
return echo . NewHTTPError ( http . StatusBadRequest , "Invalid external link scheme" )
}
}
2023-02-27 22:16:33 +08:00
2023-08-09 21:53:06 +08:00
resource , err := s . Store . CreateResource ( ctx , create )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create resource" ) . SetInternal ( err )
}
2023-11-05 21:41:47 +08:00
metric . Enqueue ( "resource create" )
2023-08-09 21:53:06 +08:00
return c . JSON ( http . StatusOK , convertResourceFromStore ( resource ) )
}
2023-01-21 08:46:49 +08:00
2023-08-09 22:30:27 +08:00
// UploadResource godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Upload resource
// @Tags resource
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "File to upload"
// @Success 200 {object} store.Resource "Created resource"
// @Failure 400 {object} nil "Upload file not found | File size exceeds allowed limit of %d MiB | Failed to parse upload data"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to get uploading file | Failed to open file | Failed to save resource | Failed to create resource | Failed to create activity"
// @Router /api/v1/resource/blob [POST]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) UploadResource ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-01-21 08:46:49 +08:00
2023-08-09 21:53:06 +08:00
// This is the backend default max upload size limit.
2023-10-26 20:21:44 +08:00
maxUploadSetting := s . Store . GetSystemSettingValueWithDefault ( ctx , SystemSettingMaxUploadSizeMiBName . String ( ) , "32" )
2023-08-09 21:53:06 +08:00
var settingMaxUploadSizeBytes int
if settingMaxUploadSizeMiB , err := strconv . Atoi ( maxUploadSetting ) ; err == nil {
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
} else {
log . Warn ( "Failed to parse max upload size" , zap . Error ( err ) )
settingMaxUploadSizeBytes = 0
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
file , err := c . FormFile ( "file" )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to get uploading file" ) . SetInternal ( err )
}
if file == nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Upload file not found" ) . SetInternal ( err )
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
if file . Size > int64 ( settingMaxUploadSizeBytes ) {
message := fmt . Sprintf ( "File size exceeds allowed limit of %d MiB" , settingMaxUploadSizeBytes / MebiByte )
return echo . NewHTTPError ( http . StatusBadRequest , message ) . SetInternal ( err )
}
if err := c . Request ( ) . ParseMultipartForm ( maxUploadBufferSizeBytes ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Failed to parse upload data" ) . SetInternal ( err )
}
2023-05-13 22:27:28 +08:00
2023-08-09 21:53:06 +08:00
sourceFile , err := file . Open ( )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to open file" ) . SetInternal ( err )
}
defer sourceFile . Close ( )
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
create := & store . Resource {
CreatorID : userID ,
Filename : file . Filename ,
Type : file . Header . Get ( "Content-Type" ) ,
Size : file . Size ,
}
err = SaveResourceBlob ( ctx , s . Store , create , sourceFile )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to save resource" ) . SetInternal ( err )
}
2022-02-03 15:32:03 +08:00
2023-08-09 21:53:06 +08:00
resource , err := s . Store . CreateResource ( ctx , create )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to create resource" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , convertResourceFromStore ( resource ) )
}
2022-02-18 22:21:10 +08:00
2023-08-09 22:30:27 +08:00
// DeleteResource godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Delete a resource
// @Tags resource
// @Produce json
// @Param resourceId path int true "Resource ID"
// @Success 200 {boolean} true "Resource deleted"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 404 {object} nil "Resource not found: %d"
// @Failure 500 {object} nil "Failed to find resource | Failed to delete resource"
// @Router /api/v1/resource/{resourceId} [DELETE]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) DeleteResource ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-04-01 16:51:20 +08:00
2023-08-09 21:53:06 +08:00
resourceID , err := util . ConvertStringToInt32 ( c . Param ( "resourceId" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "resourceId" ) ) ) . SetInternal ( err )
}
resource , err := s . Store . GetResource ( ctx , & store . FindResource {
ID : & resourceID ,
CreatorID : & userID ,
2022-06-22 19:16:31 +08:00
} )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find resource" ) . SetInternal ( err )
}
if resource == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Resource not found: %d" , resourceID ) )
}
2022-06-22 19:16:31 +08:00
2023-08-09 21:53:06 +08:00
if err := s . Store . DeleteResource ( ctx , & store . DeleteResource {
ID : resourceID ,
} ) ; err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to delete resource" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , true )
}
2022-11-06 12:21:58 +08:00
2023-08-09 22:30:27 +08:00
// UpdateResource godoc
2023-08-09 21:53:06 +08:00
//
// @Summary Update a resource
// @Tags resource
// @Produce json
// @Param resourceId path int true "Resource ID"
// @Param patch body UpdateResourceRequest true "Patch resource request"
// @Success 200 {object} store.Resource "Updated resource"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch resource request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Resource not found: %d"
// @Failure 500 {object} nil "Failed to find resource | Failed to patch resource"
// @Router /api/v1/resource/{resourceId} [PATCH]
2023-08-09 22:30:27 +08:00
func ( s * APIV1Service ) UpdateResource ( c echo . Context ) error {
2023-08-09 21:53:06 +08:00
ctx := c . Request ( ) . Context ( )
2023-09-14 20:16:17 +08:00
userID , ok := c . Get ( userIDContextKey ) . ( int32 )
2023-08-09 21:53:06 +08:00
if ! ok {
return echo . NewHTTPError ( http . StatusUnauthorized , "Missing user in session" )
}
2023-07-06 00:01:40 +08:00
2023-08-09 21:53:06 +08:00
resourceID , err := util . ConvertStringToInt32 ( c . Param ( "resourceId" ) )
if err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , fmt . Sprintf ( "ID is not a number: %s" , c . Param ( "resourceId" ) ) ) . SetInternal ( err )
}
2023-04-03 13:41:27 +08:00
2023-08-09 21:53:06 +08:00
resource , err := s . Store . GetResource ( ctx , & store . FindResource {
ID : & resourceID ,
2022-02-03 15:32:03 +08:00
} )
2023-08-09 21:53:06 +08:00
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to find resource" ) . SetInternal ( err )
}
if resource == nil {
return echo . NewHTTPError ( http . StatusNotFound , fmt . Sprintf ( "Resource not found: %d" , resourceID ) )
}
if resource . CreatorID != userID {
return echo . NewHTTPError ( http . StatusUnauthorized , "Unauthorized" )
}
2022-10-29 15:40:09 +08:00
2023-08-09 21:53:06 +08:00
request := & UpdateResourceRequest { }
if err := json . NewDecoder ( c . Request ( ) . Body ) . Decode ( request ) ; err != nil {
return echo . NewHTTPError ( http . StatusBadRequest , "Malformatted patch resource request" ) . SetInternal ( err )
}
2022-10-29 15:40:09 +08:00
2023-08-09 21:53:06 +08:00
currentTs := time . Now ( ) . Unix ( )
update := & store . UpdateResource {
ID : resourceID ,
UpdatedTs : & currentTs ,
}
if request . Filename != nil && * request . Filename != "" {
update . Filename = request . Filename
}
2022-10-29 15:40:09 +08:00
2023-08-09 21:53:06 +08:00
resource , err = s . Store . UpdateResource ( ctx , update )
if err != nil {
return echo . NewHTTPError ( http . StatusInternalServerError , "Failed to patch resource" ) . SetInternal ( err )
}
return c . JSON ( http . StatusOK , convertResourceFromStore ( resource ) )
}
2022-10-29 15:40:09 +08:00
2023-07-08 11:29:50 +08:00
func replacePathTemplate ( path , filename string ) string {
2023-03-19 19:37:57 +08:00
t := time . Now ( )
path = fileKeyPattern . ReplaceAllStringFunc ( path , func ( s string ) string {
switch s {
case "{filename}" :
return filename
case "{timestamp}" :
return fmt . Sprintf ( "%d" , t . Unix ( ) )
case "{year}" :
return fmt . Sprintf ( "%d" , t . Year ( ) )
case "{month}" :
return fmt . Sprintf ( "%02d" , t . Month ( ) )
case "{day}" :
return fmt . Sprintf ( "%02d" , t . Day ( ) )
case "{hour}" :
return fmt . Sprintf ( "%02d" , t . Hour ( ) )
case "{minute}" :
return fmt . Sprintf ( "%02d" , t . Minute ( ) )
case "{second}" :
return fmt . Sprintf ( "%02d" , t . Second ( ) )
}
return s
} )
return path
}
2023-05-20 22:08:07 +08:00
2023-07-06 00:01:40 +08:00
func convertResourceFromStore ( resource * store . Resource ) * Resource {
return & Resource {
2023-09-16 00:11:07 +08:00
ID : resource . ID ,
CreatorID : resource . CreatorID ,
CreatedTs : resource . CreatedTs ,
UpdatedTs : resource . UpdatedTs ,
Filename : resource . Filename ,
Blob : resource . Blob ,
InternalPath : resource . InternalPath ,
ExternalLink : resource . ExternalLink ,
Type : resource . Type ,
Size : resource . Size ,
2023-07-06 00:01:40 +08:00
}
}
2023-07-14 11:14:10 +08:00
// SaveResourceBlob save the blob of resource based on the storage config
//
// Depend on the storage config, some fields of *store.ResourceCreate will be changed:
// 1. *DatabaseStorage*: `create.Blob`.
// 2. *LocalStorage*: `create.InternalPath`.
// 3. Others( external service): `create.ExternalLink`.
func SaveResourceBlob ( ctx context . Context , s * store . Store , create * store . Resource , r io . Reader ) error {
systemSettingStorageServiceID , err := s . GetSystemSetting ( ctx , & store . FindSystemSetting { Name : SystemSettingStorageServiceIDName . String ( ) } )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to find SystemSettingStorageServiceIDName" )
2023-07-14 11:14:10 +08:00
}
2023-10-05 13:36:33 +08:00
storageServiceID := DefaultStorage
2023-07-14 11:14:10 +08:00
if systemSettingStorageServiceID != nil {
err = json . Unmarshal ( [ ] byte ( systemSettingStorageServiceID . Value ) , & storageServiceID )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to unmarshal storage service id" )
2023-07-14 11:14:10 +08:00
}
}
// `DatabaseStorage` means store blob into database
if storageServiceID == DatabaseStorage {
fileBytes , err := io . ReadAll ( r )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to read file" )
2023-07-14 11:14:10 +08:00
}
create . Blob = fileBytes
return nil
2023-08-25 23:10:51 +08:00
} else if storageServiceID == LocalStorage {
// `LocalStorage` means save blob into local disk
2023-07-14 11:14:10 +08:00
systemSettingLocalStoragePath , err := s . GetSystemSetting ( ctx , & store . FindSystemSetting { Name : SystemSettingLocalStoragePathName . String ( ) } )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to find SystemSettingLocalStoragePathName" )
2023-07-14 11:14:10 +08:00
}
2023-08-25 23:10:51 +08:00
localStoragePath := "assets/{timestamp}_{filename}"
2023-07-14 11:14:10 +08:00
if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath . Value != "" {
err = json . Unmarshal ( [ ] byte ( systemSettingLocalStoragePath . Value ) , & localStoragePath )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to unmarshal SystemSettingLocalStoragePathName" )
2023-07-14 11:14:10 +08:00
}
}
filePath := filepath . FromSlash ( localStoragePath )
if ! strings . Contains ( filePath , "{filename}" ) {
filePath = filepath . Join ( filePath , "{filename}" )
}
filePath = filepath . Join ( s . Profile . Data , replacePathTemplate ( filePath , create . Filename ) )
dir := filepath . Dir ( filePath )
if err = os . MkdirAll ( dir , os . ModePerm ) ; err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to create directory" )
2023-07-14 11:14:10 +08:00
}
dst , err := os . Create ( filePath )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to create file" )
2023-07-14 11:14:10 +08:00
}
defer dst . Close ( )
_ , err = io . Copy ( dst , r )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to copy file" )
2023-07-14 11:14:10 +08:00
}
create . InternalPath = filePath
return nil
}
// Others: store blob into external service, such as S3
storage , err := s . GetStorage ( ctx , & store . FindStorage { ID : & storageServiceID } )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to find StorageServiceID" )
2023-07-14 11:14:10 +08:00
}
if storage == nil {
2023-09-17 22:55:13 +08:00
return errors . Errorf ( "Storage %d not found" , storageServiceID )
2023-07-14 11:14:10 +08:00
}
storageMessage , err := ConvertStorageFromStore ( storage )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to ConvertStorageFromStore" )
2023-07-14 11:14:10 +08:00
}
if storageMessage . Type != StorageS3 {
2023-09-17 22:55:13 +08:00
return errors . Errorf ( "Unsupported storage type: %s" , storageMessage . Type )
2023-07-14 11:14:10 +08:00
}
s3Config := storageMessage . Config . S3Config
s3Client , err := s3 . NewClient ( ctx , & s3 . Config {
AccessKey : s3Config . AccessKey ,
SecretKey : s3Config . SecretKey ,
EndPoint : s3Config . EndPoint ,
Region : s3Config . Region ,
Bucket : s3Config . Bucket ,
URLPrefix : s3Config . URLPrefix ,
URLSuffix : s3Config . URLSuffix ,
} )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to create s3 client" )
2023-07-14 11:14:10 +08:00
}
filePath := s3Config . Path
if ! strings . Contains ( filePath , "{filename}" ) {
filePath = filepath . Join ( filePath , "{filename}" )
}
filePath = replacePathTemplate ( filePath , create . Filename )
link , err := s3Client . UploadFile ( ctx , filePath , create . Type , r )
if err != nil {
2023-09-29 13:04:54 +08:00
return errors . Wrap ( err , "Failed to upload via s3 client" )
2023-07-14 11:14:10 +08:00
}
create . ExternalLink = link
return nil
}