mirror of https://github.com/usememos/memos
				
				
				
			refactor: update storage setting
							parent
							
								
									f25c7d9b24
								
							
						
					
					
						commit
						320963098f
					
				@ -1,110 +0,0 @@
 | 
			
		||||
package resourcepresign
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/usememos/memos/plugin/storage/s3"
 | 
			
		||||
	storepb "github.com/usememos/memos/proto/gen/store"
 | 
			
		||||
	"github.com/usememos/memos/store"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RunPreSignLinks is a background runner that pre-signs external links stored in the database.
 | 
			
		||||
// It uses S3 client to generate presigned URLs and updates the corresponding resources in the store.
 | 
			
		||||
func RunPreSignLinks(ctx context.Context, dataStore *store.Store) {
 | 
			
		||||
	for {
 | 
			
		||||
		if err := signExternalLinks(ctx, dataStore); err != nil {
 | 
			
		||||
			slog.Error("failed to pre-sign links", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			slog.Debug("pre-signed links")
 | 
			
		||||
		}
 | 
			
		||||
		select {
 | 
			
		||||
		case <-time.After(s3.LinkLifetime / 2):
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func signExternalLinks(ctx context.Context, dataStore *store.Store) error {
 | 
			
		||||
	objectStore, err := findObjectStorage(ctx, dataStore)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrapf(err, "find object storage")
 | 
			
		||||
	}
 | 
			
		||||
	if objectStore == nil || !objectStore.Config.PreSign {
 | 
			
		||||
		// object storage not set or not supported
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resources, err := dataStore.ListResources(ctx, &store.FindResource{
 | 
			
		||||
		GetBlob: false,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrapf(err, "list resources")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, resource := range resources {
 | 
			
		||||
		if resource.ExternalLink == "" {
 | 
			
		||||
			// not for object store
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if strings.Contains(resource.ExternalLink, "?") && time.Since(time.Unix(resource.UpdatedTs, 0)) < s3.LinkLifetime/2 {
 | 
			
		||||
			// resource not signed (hack for migration)
 | 
			
		||||
			// resource was recently updated - skipping
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newLink, err := objectStore.PreSignLink(ctx, resource.ExternalLink)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Error("failed to pre-sign link", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		now := time.Now().Unix()
 | 
			
		||||
		if _, err := dataStore.UpdateResource(ctx, &store.UpdateResource{
 | 
			
		||||
			ID:           resource.ID,
 | 
			
		||||
			UpdatedTs:    &now,
 | 
			
		||||
			ExternalLink: &newLink,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return errors.Wrapf(err, "update resource %d link to %q", resource.ID, newLink)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// findObjectStorage returns current default storage if it's S3-compatible or nil otherwise.
 | 
			
		||||
// Returns error only in case of internal problems (ie: database or configuration issues).
 | 
			
		||||
// May return nil client and nil error.
 | 
			
		||||
func findObjectStorage(ctx context.Context, dataStore *store.Store) (*s3.Client, error) {
 | 
			
		||||
	workspaceStorageSetting, err := dataStore.GetWorkspaceStorageSetting(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrap(err, "Failed to find workspaceStorageSetting")
 | 
			
		||||
	}
 | 
			
		||||
	if workspaceStorageSetting.StorageType != storepb.WorkspaceStorageSetting_STORAGE_TYPE_EXTERNAL || workspaceStorageSetting.ActivedExternalStorageId == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	storage, err := dataStore.GetStorage(ctx, &store.FindStorage{ID: workspaceStorageSetting.ActivedExternalStorageId})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrap(err, "Failed to find storage")
 | 
			
		||||
	}
 | 
			
		||||
	if storage == nil || storage.Type != storepb.Storage_S3 {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s3Config := storage.Config.GetS3Config()
 | 
			
		||||
	return 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,
 | 
			
		||||
		PreSign:   s3Config.PreSign,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@ -1,257 +0,0 @@
 | 
			
		||||
import { Button, IconButton, Input, Checkbox, Typography } from "@mui/joy";
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
import { toast } from "react-hot-toast";
 | 
			
		||||
import { storageServiceClient } from "@/grpcweb";
 | 
			
		||||
import { S3Config, Storage, Storage_Type } from "@/types/proto/api/v1/storage_service";
 | 
			
		||||
import { useTranslate } from "@/utils/i18n";
 | 
			
		||||
import { generateDialog } from "./Dialog";
 | 
			
		||||
import Icon from "./Icon";
 | 
			
		||||
import LearnMore from "./LearnMore";
 | 
			
		||||
import RequiredBadge from "./RequiredBadge";
 | 
			
		||||
 | 
			
		||||
interface Props extends DialogProps {
 | 
			
		||||
  storage?: Storage;
 | 
			
		||||
  confirmCallback?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
 | 
			
		||||
  const t = useTranslate();
 | 
			
		||||
  const { destroy, storage, confirmCallback } = props;
 | 
			
		||||
  const [basicInfo, setBasicInfo] = useState({
 | 
			
		||||
    title: "",
 | 
			
		||||
  });
 | 
			
		||||
  const [type] = useState<Storage_Type>(Storage_Type.S3);
 | 
			
		||||
  const [s3Config, setS3Config] = useState<S3Config>({
 | 
			
		||||
    endPoint: "",
 | 
			
		||||
    region: "",
 | 
			
		||||
    accessKey: "",
 | 
			
		||||
    secretKey: "",
 | 
			
		||||
    path: "",
 | 
			
		||||
    bucket: "",
 | 
			
		||||
    urlPrefix: "",
 | 
			
		||||
    urlSuffix: "",
 | 
			
		||||
    preSign: false,
 | 
			
		||||
  });
 | 
			
		||||
  const isCreating = storage === undefined;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (storage) {
 | 
			
		||||
      setBasicInfo({
 | 
			
		||||
        title: storage.title,
 | 
			
		||||
      });
 | 
			
		||||
      if (storage.type === "S3") {
 | 
			
		||||
        setS3Config(S3Config.fromPartial(storage.config?.s3Config || {}));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleCloseBtnClick = () => {
 | 
			
		||||
    destroy();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const allowConfirmAction = () => {
 | 
			
		||||
    if (basicInfo.title === "") {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (type === "S3") {
 | 
			
		||||
      if (
 | 
			
		||||
        s3Config.endPoint === "" ||
 | 
			
		||||
        s3Config.region === "" ||
 | 
			
		||||
        s3Config.accessKey === "" ||
 | 
			
		||||
        s3Config.secretKey === "" ||
 | 
			
		||||
        s3Config.bucket === ""
 | 
			
		||||
      ) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleConfirmBtnClick = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (isCreating) {
 | 
			
		||||
        await storageServiceClient.createStorage({
 | 
			
		||||
          storage: Storage.fromPartial({
 | 
			
		||||
            title: basicInfo.title,
 | 
			
		||||
            type: type,
 | 
			
		||||
            config: {
 | 
			
		||||
              s3Config: s3Config,
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        await storageServiceClient.updateStorage({
 | 
			
		||||
          storage: Storage.fromPartial({
 | 
			
		||||
            id: storage?.id,
 | 
			
		||||
            title: basicInfo.title,
 | 
			
		||||
            type: type,
 | 
			
		||||
            config: {
 | 
			
		||||
              s3Config: s3Config,
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
          updateMask: ["title", "config"],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      toast.error(error.response.data.message);
 | 
			
		||||
    }
 | 
			
		||||
    if (confirmCallback) {
 | 
			
		||||
      confirmCallback();
 | 
			
		||||
    }
 | 
			
		||||
    destroy();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const setPartialS3Config = (state: Partial<S3Config>) => {
 | 
			
		||||
    setS3Config({
 | 
			
		||||
      ...s3Config,
 | 
			
		||||
      ...state,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="dialog-header-container">
 | 
			
		||||
        <span>{t(isCreating ? "setting.storage-section.create-storage" : "setting.storage-section.update-storage")}</span>
 | 
			
		||||
        <IconButton size="sm" onClick={handleCloseBtnClick}>
 | 
			
		||||
          <Icon.X className="w-5 h-auto" />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="dialog-content-container min-w-[19rem]">
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("common.name")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("common.name")}
 | 
			
		||||
          value={basicInfo.title}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
            setBasicInfo({
 | 
			
		||||
              ...basicInfo,
 | 
			
		||||
              title: e.target.value,
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.endpoint")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.s3-compatible-url")}
 | 
			
		||||
          value={s3Config.endPoint}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ endPoint: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.region")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.region-placeholder")}
 | 
			
		||||
          value={s3Config.region}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ region: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.accesskey")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.accesskey-placeholder")}
 | 
			
		||||
          value={s3Config.accessKey}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ accessKey: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.secretkey")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.secretkey-placeholder")}
 | 
			
		||||
          value={s3Config.secretKey}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ secretKey: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.bucket")}
 | 
			
		||||
          <RequiredBadge />
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.bucket-placeholder")}
 | 
			
		||||
          value={s3Config.bucket}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <div className="flex flex-row items-center mb-1">
 | 
			
		||||
          <Typography level="body-md">{t("setting.storage-section.path")}</Typography>
 | 
			
		||||
          <LearnMore
 | 
			
		||||
            className="ml-1"
 | 
			
		||||
            title={t("setting.storage-section.path-description")}
 | 
			
		||||
            url="https://usememos.com/docs/advanced-settings/local-storage"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.path-placeholder") + "/{year}/{month}/{filename}"}
 | 
			
		||||
          value={s3Config.path}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ path: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.url-prefix")}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.url-prefix-placeholder")}
 | 
			
		||||
          value={s3Config.urlPrefix}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ urlPrefix: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Typography className="!mb-1" level="body-md">
 | 
			
		||||
          {t("setting.storage-section.url-suffix")}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Input
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          placeholder={t("setting.storage-section.url-suffix-placeholder")}
 | 
			
		||||
          value={s3Config.urlSuffix}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ urlSuffix: e.target.value })}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          className="mb-2"
 | 
			
		||||
          label={t("setting.storage-section.presign-placeholder")}
 | 
			
		||||
          checked={s3Config.preSign}
 | 
			
		||||
          onChange={(e) => setPartialS3Config({ preSign: e.target.checked })}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
 | 
			
		||||
          <Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
 | 
			
		||||
            {t("common.cancel")}
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={handleConfirmBtnClick} disabled={!allowConfirmAction()}>
 | 
			
		||||
            {t(isCreating ? "common.create" : "common.update")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function showCreateStorageServiceDialog(storage?: Storage, confirmCallback?: () => void) {
 | 
			
		||||
  generateDialog(
 | 
			
		||||
    {
 | 
			
		||||
      className: "create-storage-service-dialog",
 | 
			
		||||
      dialogName: "create-storage-service-dialog",
 | 
			
		||||
    },
 | 
			
		||||
    CreateStorageServiceDialog,
 | 
			
		||||
    { storage, confirmCallback },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default showCreateStorageServiceDialog;
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue