mirror of
https://github.com/usememos/memos.git
synced 2025-01-14 16:43:07 +08:00
feat: support local storage (#1383)
* feat: support local storage * update * update * update * update
This commit is contained in:
parent
a21ff5c2e3
commit
f3090b115d
10 changed files with 224 additions and 48 deletions
|
@ -11,6 +11,7 @@ type Resource struct {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Blob []byte `json:"-"`
|
Blob []byte `json:"-"`
|
||||||
|
InternalPath string `json:"internalPath"`
|
||||||
ExternalLink string `json:"externalLink"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
@ -27,6 +28,7 @@ type ResourceCreate struct {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Blob []byte `json:"-"`
|
Blob []byte `json:"-"`
|
||||||
|
InternalPath string `json:"internalPath"`
|
||||||
ExternalLink string `json:"externalLink"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Size int64 `json:"-"`
|
Size int64 `json:"-"`
|
||||||
|
|
|
@ -18,5 +18,8 @@ type SystemStatus struct {
|
||||||
AdditionalScript string `json:"additionalScript"`
|
AdditionalScript string `json:"additionalScript"`
|
||||||
// Customized server profile, including server name and external url.
|
// Customized server profile, including server name and external url.
|
||||||
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
||||||
|
// Storage service ID.
|
||||||
StorageServiceID int `json:"storageServiceId"`
|
StorageServiceID int `json:"storageServiceId"`
|
||||||
|
// Local storage path
|
||||||
|
LocalStoragePath string `json:"localStoragePath"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ const (
|
||||||
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
|
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
|
||||||
// SystemSettingStorageServiceIDName is the key type of storage service ID.
|
// SystemSettingStorageServiceIDName is the key type of storage service ID.
|
||||||
SystemSettingStorageServiceIDName SystemSettingName = "storageServiceId"
|
SystemSettingStorageServiceIDName SystemSettingName = "storageServiceId"
|
||||||
|
// SystemSettingLocalStoragePathName is the key type of local storage path.
|
||||||
|
SystemSettingLocalStoragePathName SystemSettingName = "localStoragePath"
|
||||||
// SystemSettingOpenAIConfigName is the key type of OpenAI config.
|
// SystemSettingOpenAIConfigName is the key type of OpenAI config.
|
||||||
SystemSettingOpenAIConfigName SystemSettingName = "openAIConfig"
|
SystemSettingOpenAIConfigName SystemSettingName = "openAIConfig"
|
||||||
)
|
)
|
||||||
|
@ -70,6 +72,8 @@ func (key SystemSettingName) String() string {
|
||||||
return "customizedProfile"
|
return "customizedProfile"
|
||||||
case SystemSettingStorageServiceIDName:
|
case SystemSettingStorageServiceIDName:
|
||||||
return "storageServiceId"
|
return "storageServiceId"
|
||||||
|
case SystemSettingLocalStoragePathName:
|
||||||
|
return "localStoragePath"
|
||||||
case SystemSettingOpenAIConfigName:
|
case SystemSettingOpenAIConfigName:
|
||||||
return "openAIConfig"
|
return "openAIConfig"
|
||||||
}
|
}
|
||||||
|
@ -142,6 +146,12 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||||
return fmt.Errorf("failed to unmarshal system setting storage service id value")
|
return fmt.Errorf("failed to unmarshal system setting storage service id value")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
} else if upsert.Name == SystemSettingLocalStoragePathName {
|
||||||
|
value := ""
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal system setting local storage path value")
|
||||||
|
}
|
||||||
} else if upsert.Name == SystemSettingOpenAIConfigName {
|
} else if upsert.Name == SystemSettingOpenAIConfigName {
|
||||||
value := OpenAIConfig{}
|
value := OpenAIConfig{}
|
||||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||||
|
|
|
@ -7,7 +7,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -105,13 +107,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer src.Close()
|
||||||
|
|
||||||
systemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingStorageServiceIDName})
|
systemSettingStorageServiceID, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingStorageServiceIDName})
|
||||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
}
|
}
|
||||||
storageServiceID := 0
|
storageServiceID := 0
|
||||||
if systemSetting != nil {
|
if systemSettingStorageServiceID != nil {
|
||||||
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
|
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
@ -119,6 +121,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
|
|
||||||
var resourceCreate *api.ResourceCreate
|
var resourceCreate *api.ResourceCreate
|
||||||
if storageServiceID == 0 {
|
if storageServiceID == 0 {
|
||||||
|
// Database storage.
|
||||||
fileBytes, err := io.ReadAll(src)
|
fileBytes, err := io.ReadAll(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
||||||
|
@ -130,6 +133,47 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
Size: size,
|
Size: size,
|
||||||
Blob: fileBytes,
|
Blob: fileBytes,
|
||||||
}
|
}
|
||||||
|
} else if storageServiceID == -1 {
|
||||||
|
// Local storage.
|
||||||
|
systemSettingLocalStoragePath, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingLocalStoragePathName})
|
||||||
|
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
localStoragePath := ""
|
||||||
|
if systemSettingLocalStoragePath != nil {
|
||||||
|
err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filePath := localStoragePath
|
||||||
|
if !strings.Contains(filePath, "{filename}") {
|
||||||
|
filePath = path.Join(filePath, "{filename}")
|
||||||
|
}
|
||||||
|
filePath = path.Join(s.Profile.Data, replacePathTemplate(filePath, filename))
|
||||||
|
dirPath := filepath.Dir(filePath)
|
||||||
|
err = os.MkdirAll(dirPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create directory").SetInternal(err)
|
||||||
|
}
|
||||||
|
dst, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create file").SetInternal(err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(dst, src)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to copy file").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceCreate = &api.ResourceCreate{
|
||||||
|
CreatorID: userID,
|
||||||
|
Filename: filename,
|
||||||
|
Type: filetype,
|
||||||
|
Size: size,
|
||||||
|
InternalPath: filePath,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{ID: &storageServiceID})
|
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{ID: &storageServiceID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -138,38 +182,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
|
|
||||||
if storage.Type == api.StorageS3 {
|
if storage.Type == api.StorageS3 {
|
||||||
s3Config := storage.Config.S3Config
|
s3Config := storage.Config.S3Config
|
||||||
t := time.Now()
|
|
||||||
var s3FileKey string
|
var s3FileKey string
|
||||||
if s3Config.Path == "" {
|
|
||||||
s3FileKey = filename
|
|
||||||
} else {
|
|
||||||
s3FileKey = fileKeyPattern.ReplaceAllStringFunc(s3Config.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
|
|
||||||
})
|
|
||||||
|
|
||||||
if !strings.Contains(s3Config.Path, "{filename}") {
|
if !strings.Contains(s3Config.Path, "{filename}") {
|
||||||
s3FileKey = path.Join(s3FileKey, filename)
|
s3FileKey = path.Join(s3Config.Path, "{filename}")
|
||||||
}
|
}
|
||||||
}
|
s3FileKey = replacePathTemplate(s3FileKey, filename)
|
||||||
|
|
||||||
s3client, err := s3.NewClient(ctx, &s3.Config{
|
s3client, err := s3.NewClient(ctx, &s3.Config{
|
||||||
AccessKey: s3Config.AccessKey,
|
AccessKey: s3Config.AccessKey,
|
||||||
SecretKey: s3Config.SecretKey,
|
SecretKey: s3Config.SecretKey,
|
||||||
|
@ -387,16 +404,29 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blob := resource.Blob
|
||||||
|
if resource.InternalPath != "" {
|
||||||
|
src, err := os.Open(resource.InternalPath)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", resource.InternalPath)).SetInternal(err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
blob, err = io.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to read the local resource: %s", resource.InternalPath)).SetInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||||
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
|
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
|
||||||
resourceType := strings.ToLower(resource.Type)
|
resourceType := strings.ToLower(resource.Type)
|
||||||
if strings.HasPrefix(resourceType, "text") {
|
if strings.HasPrefix(resourceType, "text") {
|
||||||
resourceType = echo.MIMETextPlainCharsetUTF8
|
resourceType = echo.MIMETextPlainCharsetUTF8
|
||||||
} else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
|
} else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
|
||||||
http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(resource.Blob))
|
http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(blob))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.Stream(http.StatusOK, resourceType, bytes.NewReader(resource.Blob))
|
return c.Stream(http.StatusOK, resourceType, bytes.NewReader(blob))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,3 +452,29 @@ func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Reso
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func replacePathTemplate(path string, filename string) string {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||||
ExternalURL: "",
|
ExternalURL: "",
|
||||||
},
|
},
|
||||||
StorageServiceID: 0,
|
StorageServiceID: 0,
|
||||||
|
LocalStoragePath: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
||||||
|
@ -86,6 +87,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||||
systemStatus.CustomizedProfile = customizedProfile
|
systemStatus.CustomizedProfile = customizedProfile
|
||||||
} else if systemSetting.Name == api.SystemSettingStorageServiceIDName {
|
} else if systemSetting.Name == api.SystemSettingStorageServiceIDName {
|
||||||
systemStatus.StorageServiceID = int(baseValue.(float64))
|
systemStatus.StorageServiceID = int(baseValue.(float64))
|
||||||
|
} else if systemSetting.Name == api.SystemSettingLocalStoragePathName {
|
||||||
|
systemStatus.LocalStoragePath = baseValue.(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ CREATE TABLE resource (
|
||||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
filename TEXT NOT NULL DEFAULT '',
|
filename TEXT NOT NULL DEFAULT '',
|
||||||
blob BLOB DEFAULT NULL,
|
blob BLOB DEFAULT NULL,
|
||||||
|
internal_path TEXT NOT NULL DEFAULT '',
|
||||||
external_link TEXT NOT NULL DEFAULT '',
|
external_link TEXT NOT NULL DEFAULT '',
|
||||||
type TEXT NOT NULL DEFAULT '',
|
type TEXT NOT NULL DEFAULT '',
|
||||||
size INTEGER NOT NULL DEFAULT 0,
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
|
@ -24,6 +24,7 @@ type resourceRaw struct {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string
|
Filename string
|
||||||
Blob []byte
|
Blob []byte
|
||||||
|
InternalPath string
|
||||||
ExternalLink string
|
ExternalLink string
|
||||||
Type string
|
Type string
|
||||||
Size int64
|
Size int64
|
||||||
|
@ -43,6 +44,7 @@ func (raw *resourceRaw) toResource() *api.Resource {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename: raw.Filename,
|
Filename: raw.Filename,
|
||||||
Blob: raw.Blob,
|
Blob: raw.Blob,
|
||||||
|
InternalPath: raw.InternalPath,
|
||||||
ExternalLink: raw.ExternalLink,
|
ExternalLink: raw.ExternalLink,
|
||||||
Type: raw.Type,
|
Type: raw.Type,
|
||||||
Size: raw.Size,
|
Size: raw.Size,
|
||||||
|
@ -195,9 +197,9 @@ func (s *Store) createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.
|
||||||
values := []any{create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID}
|
values := []any{create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID}
|
||||||
placeholders := []string{"?", "?", "?", "?", "?", "?"}
|
placeholders := []string{"?", "?", "?", "?", "?", "?"}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "visibility")
|
fields = append(fields, "visibility", "internal_path")
|
||||||
values = append(values, create.Visibility)
|
values = append(values, create.Visibility, create.InternalPath)
|
||||||
placeholders = append(placeholders, "?")
|
placeholders = append(placeholders, "?", "?")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
|
@ -218,7 +220,7 @@ func (s *Store) createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.
|
||||||
&resourceRaw.CreatorID,
|
&resourceRaw.CreatorID,
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.Visibility)
|
dests = append(dests, &resourceRaw.Visibility, &resourceRaw.InternalPath)
|
||||||
}
|
}
|
||||||
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
|
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
|
||||||
if err := tx.QueryRowContext(ctx, query, values...).Scan(dests...); err != nil {
|
if err := tx.QueryRowContext(ctx, query, values...).Scan(dests...); err != nil {
|
||||||
|
@ -247,7 +249,7 @@ func (s *Store) patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.Re
|
||||||
|
|
||||||
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts"}
|
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts"}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "visibility")
|
fields = append(fields, "visibility", "internal_path")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
|
@ -267,7 +269,7 @@ func (s *Store) patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.Re
|
||||||
&resourceRaw.UpdatedTs,
|
&resourceRaw.UpdatedTs,
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.Visibility)
|
dests = append(dests, &resourceRaw.Visibility, &resourceRaw.InternalPath)
|
||||||
}
|
}
|
||||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil {
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
|
@ -297,7 +299,7 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
||||||
fields = append(fields, "resource.blob")
|
fields = append(fields, "resource.blob")
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "resource.visibility")
|
fields = append(fields, "visibility", "internal_path")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
|
@ -334,7 +336,7 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
||||||
dests = append(dests, &resourceRaw.Blob)
|
dests = append(dests, &resourceRaw.Blob)
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.Visibility)
|
dests = append(dests, &resourceRaw.Visibility, &resourceRaw.InternalPath)
|
||||||
}
|
}
|
||||||
if err := rows.Scan(dests...); err != nil {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useGlobalStore } from "../../store/module";
|
import { useGlobalStore } from "../../store/module";
|
||||||
import * as api from "../../helpers/api";
|
import * as api from "../../helpers/api";
|
||||||
import showCreateStorageServiceDialog from "../CreateStorageServiceDialog";
|
import showCreateStorageServiceDialog from "../CreateStorageServiceDialog";
|
||||||
|
import showUpdateLocalStorageDialog from "../UpdateLocalStorageDialog";
|
||||||
import Dropdown from "../base/Dropdown";
|
import Dropdown from "../base/Dropdown";
|
||||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||||
|
|
||||||
|
@ -27,10 +28,6 @@ const StorageSection = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleActiveStorageServiceChanged = async (storageId: StorageId) => {
|
const handleActiveStorageServiceChanged = async (storageId: StorageId) => {
|
||||||
if (storageList.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.upsertSystemSetting({
|
await api.upsertSystemSetting({
|
||||||
name: "storageServiceId",
|
name: "storageServiceId",
|
||||||
value: JSON.stringify(storageId),
|
value: JSON.stringify(storageId),
|
||||||
|
@ -70,6 +67,7 @@ const StorageSection = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Option value={0}>Database</Option>
|
<Option value={0}>Database</Option>
|
||||||
|
<Option value={-1}>Local</Option>
|
||||||
{storageList.map((storage) => (
|
{storageList.map((storage) => (
|
||||||
<Option key={storage.id} value={storage.id}>
|
<Option key={storage.id} value={storage.id}>
|
||||||
{storage.name}
|
{storage.name}
|
||||||
|
@ -84,6 +82,26 @@ const StorageSection = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 w-full flex flex-col">
|
<div className="mt-2 w-full flex flex-col">
|
||||||
|
<div className="py-2 w-full border-t last:border-b flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<p className="ml-2">Local</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Dropdown
|
||||||
|
actionsClassName="!w-28"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
onClick={() => showUpdateLocalStorageDialog(systemStatus.localStoragePath)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{storageList.map((storage) => (
|
{storageList.map((storage) => (
|
||||||
<div key={storage.id} className="py-2 w-full border-t last:border-b flex flex-row items-center justify-between">
|
<div key={storage.id} className="py-2 w-full border-t last:border-b flex flex-row items-center justify-between">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
|
|
80
web/src/components/UpdateLocalStorageDialog.tsx
Normal file
80
web/src/components/UpdateLocalStorageDialog.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button, Input, Typography } from "@mui/joy";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import * as api from "../helpers/api";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { useGlobalStore } from "../store/module";
|
||||||
|
|
||||||
|
interface Props extends DialogProps {
|
||||||
|
localStoragePath?: string;
|
||||||
|
confirmCallback?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateLocalStorageDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { destroy, localStoragePath, confirmCallback } = props;
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const [path, setPath] = useState(localStoragePath || "");
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmBtnClick = async () => {
|
||||||
|
try {
|
||||||
|
await api.upsertSystemSetting({
|
||||||
|
name: "localStoragePath",
|
||||||
|
value: JSON.stringify(path),
|
||||||
|
});
|
||||||
|
await globalStore.fetchSystemStatus();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.response.data.message);
|
||||||
|
}
|
||||||
|
if (confirmCallback) {
|
||||||
|
confirmCallback();
|
||||||
|
}
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container">
|
||||||
|
<p className="title-text">Update local storage path</p>
|
||||||
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
|
<Icon.X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
<div className="py-2">
|
||||||
|
<Typography className="!mb-1" level="body2">
|
||||||
|
Local Path
|
||||||
|
</Typography>
|
||||||
|
<Typography className="!mb-1" level="body2">
|
||||||
|
<span className="text-sm text-gray-400 ml-1">{"e.g., {year}/{month}/{day}/your/path/{timestamp}_{filename}"}</span>
|
||||||
|
</Typography>
|
||||||
|
<Input className="mb-2" placeholder="Path" value={path} onChange={(e) => setPath(e.target.value)} fullWidth />
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
|
||||||
|
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmBtnClick}>Update</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function showUpdateLocalStorageDialog(localStoragePath?: string, confirmCallback?: () => void) {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "update-local-storage-dialog",
|
||||||
|
dialogName: "update-local-storage-dialog",
|
||||||
|
},
|
||||||
|
UpdateLocalStorageDialog,
|
||||||
|
{ localStoragePath, confirmCallback }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showUpdateLocalStorageDialog;
|
1
web/src/types/modules/system.d.ts
vendored
1
web/src/types/modules/system.d.ts
vendored
|
@ -28,6 +28,7 @@ interface SystemStatus {
|
||||||
additionalScript: string;
|
additionalScript: string;
|
||||||
customizedProfile: CustomizedProfile;
|
customizedProfile: CustomizedProfile;
|
||||||
storageServiceId: number;
|
storageServiceId: number;
|
||||||
|
localStoragePath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemSetting {
|
interface SystemSetting {
|
||||||
|
|
Loading…
Reference in a new issue