mirror of
https://github.com/usememos/memos.git
synced 2024-11-27 19:30:59 +03:00
feat: vacuum records manually (#420)
This commit is contained in:
parent
4f10c12092
commit
dc5d705f8c
@ -90,5 +90,5 @@ type MemoFind struct {
|
||||
}
|
||||
|
||||
type MemoDelete struct {
|
||||
ID int `json:"id"`
|
||||
ID int
|
||||
}
|
||||
|
@ -19,3 +19,8 @@ type MemoOrganizerUpsert struct {
|
||||
UserID int
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type MemoOrganizerDelete struct {
|
||||
MemoID *int
|
||||
UserID *int
|
||||
}
|
||||
|
@ -19,6 +19,6 @@ type MemoResourceFind struct {
|
||||
}
|
||||
|
||||
type MemoResourceDelete struct {
|
||||
MemoID int
|
||||
MemoID *int
|
||||
ResourceID *int
|
||||
}
|
||||
|
@ -40,18 +40,16 @@ type ResourceFind struct {
|
||||
MemoID *int
|
||||
}
|
||||
|
||||
type ResourceDelete struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
}
|
||||
|
||||
type ResourcePatch struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
UpdatedTs *int64
|
||||
|
||||
// Domain specific fields
|
||||
Filename *string `json:"filename"`
|
||||
}
|
||||
|
||||
type ResourceDelete struct {
|
||||
ID int
|
||||
}
|
||||
|
@ -46,5 +46,8 @@ type ShortcutFind struct {
|
||||
}
|
||||
|
||||
type ShortcutDelete struct {
|
||||
ID int
|
||||
ID *int
|
||||
|
||||
// Standard fields
|
||||
CreatorID *int
|
||||
}
|
||||
|
@ -156,3 +156,7 @@ type UserSettingFind struct {
|
||||
|
||||
Key *UserSettingKey `json:"key"`
|
||||
}
|
||||
|
||||
type UserSettingDelete struct {
|
||||
UserID int
|
||||
}
|
||||
|
@ -500,7 +500,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
memoResourceDelete := &api.MemoResourceDelete{
|
||||
MemoID: memoID,
|
||||
MemoID: &memoID,
|
||||
ResourceID: &resourceID,
|
||||
}
|
||||
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
|
||||
|
@ -170,9 +170,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
CreatorID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
|
||||
}
|
||||
if resource == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "Not find resource").SetInternal(err)
|
||||
}
|
||||
|
||||
resourceDelete := &api.ResourceDelete{
|
||||
ID: resourceID,
|
||||
CreatorID: userID,
|
||||
ID: resourceID,
|
||||
}
|
||||
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
|
@ -128,7 +128,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
shortcutDelete := &api.ShortcutDelete{
|
||||
ID: shortcutID,
|
||||
ID: &shortcutID,
|
||||
}
|
||||
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
|
@ -221,13 +221,8 @@ func (s *Store) DeleteMemo(ctx context.Context, delete *api.MemoDelete) error {
|
||||
if err := deleteMemo(ctx, tx, delete); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
resourceDelete := &api.MemoResourceDelete{
|
||||
MemoID: delete.ID,
|
||||
}
|
||||
|
||||
if err := deleteMemoResource(ctx, tx, resourceDelete); err != nil {
|
||||
return FormatError(err)
|
||||
if err := vacuum(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@ -323,7 +318,7 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
|
||||
where, args = append(where, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Pinned; v != nil {
|
||||
where = append(where, "id in (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)")
|
||||
where = append(where, "id IN (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)")
|
||||
}
|
||||
if v := find.ContentSearch; v != nil {
|
||||
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
|
||||
@ -382,16 +377,36 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
|
||||
}
|
||||
|
||||
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM memo WHERE id = ?
|
||||
`, delete.ID)
|
||||
where, args := []string{"id = ?"}, []interface{}{delete.ID}
|
||||
|
||||
stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.ID)}
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
memo
|
||||
WHERE
|
||||
creator_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
user
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
@ -65,6 +66,24 @@ func (s *Store) UpsertMemoOrganizer(ctx context.Context, upsert *api.MemoOrganiz
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteMemoOrganizer(ctx context.Context, delete *api.MemoOrganizerDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := deleteMemoOrganizer(ctx, tx, delete); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
|
||||
query := `
|
||||
SELECT
|
||||
@ -127,3 +146,52 @@ func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrgani
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteMemoOrganizer(ctx context.Context, tx *sql.Tx, delete *api.MemoOrganizerDelete) error {
|
||||
where, args := []string{}, []interface{}{}
|
||||
|
||||
if v := delete.MemoID; v != nil {
|
||||
where, args = append(where, "memo_id = ?"), append(args, *v)
|
||||
}
|
||||
if v := delete.UserID; v != nil {
|
||||
where, args = append(where, "user_id = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
stmt := `DELETE FROM memo_organizer WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo organizer not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumMemoOrganizer(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
memo_organizer
|
||||
WHERE
|
||||
memo_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
memo
|
||||
)
|
||||
OR user_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
user
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -188,14 +188,17 @@ func upsertMemoResource(ctx context.Context, tx *sql.Tx, upsert *api.MemoResourc
|
||||
}
|
||||
|
||||
func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourceDelete) error {
|
||||
where, args := []string{"memo_id = ?"}, []interface{}{delete.MemoID}
|
||||
where, args := []string{}, []interface{}{}
|
||||
|
||||
if v := delete.MemoID; v != nil {
|
||||
where, args = append(where, "memo_id = ?"), append(args, *v)
|
||||
}
|
||||
if v := delete.ResourceID; v != nil {
|
||||
where, args = append(where, "resource_id = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM memo_resource WHERE `+strings.Join(where, " AND "), args...)
|
||||
stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
@ -207,3 +210,28 @@ func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
memo_resource
|
||||
WHERE
|
||||
memo_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
memo
|
||||
)
|
||||
OR resource_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
resource
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -169,8 +169,10 @@ func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteResource(ctx, tx, delete)
|
||||
if err != nil {
|
||||
if err := deleteResource(ctx, tx, delete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuum(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -178,11 +180,6 @@ func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete)
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
// Vacuum sqlite database file size after deleting resource.
|
||||
if _, err := s.db.Exec("VACUUM"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ResourceCache, delete.ID)
|
||||
|
||||
return nil
|
||||
@ -340,16 +337,36 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
|
||||
}
|
||||
|
||||
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM resource WHERE id = ? AND creator_id = ?
|
||||
`, delete.ID, delete.CreatorID)
|
||||
where, args := []string{"id = ?"}, []interface{}{delete.ID}
|
||||
|
||||
stmt := `DELETE FROM resource WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.ID)}
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumResource(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
resource
|
||||
WHERE
|
||||
creator_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
user
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -164,7 +164,7 @@ func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete)
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ShortcutCache, delete.ID)
|
||||
s.cache.DeleteCache(api.ShortcutCache, *delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -292,16 +292,43 @@ func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) (
|
||||
}
|
||||
|
||||
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM shortcut WHERE id = ?
|
||||
`, delete.ID)
|
||||
where, args := []string{}, []interface{}{}
|
||||
|
||||
if v := delete.ID; v != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *v)
|
||||
}
|
||||
if v := delete.CreatorID; v != nil {
|
||||
where, args = append(where, "creator_id = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
stmt := `DELETE FROM shortcut WHERE ` + strings.Join(where, " AND ")
|
||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)}
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
shortcut
|
||||
WHERE
|
||||
creator_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
user
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
@ -24,3 +25,51 @@ func New(db *sql.DB, profile *profile.Profile) *Store {
|
||||
cache: cacheService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Vacuum(ctx context.Context) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := vacuum(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
// Vacuum sqlite database file size after deleting resource.
|
||||
if _, err := s.db.Exec("VACUUM"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec vacuum records in a transcation.
|
||||
func vacuum(ctx context.Context, tx *sql.Tx) error {
|
||||
if err := vacuumMemo(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuumResource(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuumShortcut(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuumUserSetting(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuumMemoResource(ctx, tx); err != nil {
|
||||
// Prevent revive warning.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -175,13 +175,15 @@ func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteUser(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
if err := deleteUser(ctx, tx, delete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := vacuum(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.UserCache, delete.ID)
|
||||
@ -364,7 +366,7 @@ func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)}
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -149,3 +149,22 @@ func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingF
|
||||
|
||||
return userSettingRawList, nil
|
||||
}
|
||||
|
||||
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {
|
||||
stmt := `
|
||||
DELETE FROM
|
||||
user_setting
|
||||
WHERE
|
||||
user_id NOT IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
user
|
||||
)`
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user