feature: move docs
This commit is contained in:
@@ -27,12 +27,25 @@ type Service struct {
|
||||
}
|
||||
|
||||
type DocumentChange struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
DocumentID string `json:"documentId"`
|
||||
Path string `json:"path"`
|
||||
OldPath string `json:"oldPath,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Hash string `json:"hash"`
|
||||
PreviousHash string `json:"previousHash,omitempty"`
|
||||
}
|
||||
|
||||
type DocumentMoveConflictError struct {
|
||||
OldPath string
|
||||
NewPath string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *DocumentMoveConflictError) Error() string {
|
||||
return fmt.Sprintf("cannot move %s to %s: %s", e.OldPath, e.NewPath, e.Reason)
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Path string
|
||||
Title string
|
||||
@@ -146,9 +159,10 @@ func (s *Service) ArchiveDocument(ctx context.Context, path string) error {
|
||||
|
||||
if s.onChange != nil {
|
||||
s.onChange(DocumentChange{
|
||||
Path: record.Path,
|
||||
Title: record.Title,
|
||||
Hash: record.CurrentHash,
|
||||
DocumentID: record.ID,
|
||||
Path: record.Path,
|
||||
Title: record.Title,
|
||||
Hash: record.CurrentHash,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,6 +184,96 @@ func (s *Service) RestoreDocument(ctx context.Context, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) MoveDocument(ctx context.Context, requestPath, destinationPath, expectedHash string) (*DocumentRecord, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, err := s.syncSourceDirLocked(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldPath, err := NormalizeDocumentPath(requestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newPath, err := NormalizeDocumentPath(destinationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oldPath == newPath {
|
||||
return nil, &DocumentMoveConflictError{OldPath: oldPath, NewPath: newPath, Reason: "destination is unchanged"}
|
||||
}
|
||||
|
||||
record, err := s.repo.GetDocumentByPath(ctx, oldPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expectedHash != "" && record.CurrentHash != expectedHash {
|
||||
return nil, &DocumentMoveConflictError{OldPath: oldPath, NewPath: newPath, Reason: "document changed since the page loaded"}
|
||||
}
|
||||
if _, err := s.repo.GetDocumentByPathIncludingArchived(ctx, newPath); err == nil {
|
||||
return nil, &DocumentMoveConflictError{OldPath: oldPath, NewPath: newPath, Reason: "destination already exists"}
|
||||
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if aliased, err := s.repo.GetDocumentByPathOrAlias(ctx, newPath); err == nil && aliased.ID != record.ID {
|
||||
return nil, &DocumentMoveConflictError{OldPath: oldPath, NewPath: newPath, Reason: "destination is reserved by another document"}
|
||||
} else if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldFullPath := filepath.Join(s.sourceDir, filepath.FromSlash(oldPath))
|
||||
newFullPath := filepath.Join(s.sourceDir, filepath.FromSlash(newPath))
|
||||
if _, err := os.Stat(newFullPath); err == nil {
|
||||
return nil, &DocumentMoveConflictError{OldPath: oldPath, NewPath: newPath, Reason: "destination file already exists"}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("check destination file: %w", err)
|
||||
}
|
||||
content, err := os.ReadFile(oldFullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read source document: %w", err)
|
||||
}
|
||||
rendered, err := s.renderer.RenderPath(content, newPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("render moved document: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(newFullPath), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create destination folder: %w", err)
|
||||
}
|
||||
if err := os.Rename(oldFullPath, newFullPath); err != nil {
|
||||
return nil, fmt.Errorf("move document file: %w", err)
|
||||
}
|
||||
|
||||
moveErr := s.repo.MoveDocument(ctx, MoveDocumentInput{
|
||||
ID: record.ID,
|
||||
OldPath: oldPath,
|
||||
NewPath: newPath,
|
||||
ExpectedHash: record.CurrentHash,
|
||||
Title: rendered.Title,
|
||||
})
|
||||
if moveErr != nil {
|
||||
if rollbackErr := os.Rename(newFullPath, oldFullPath); rollbackErr != nil {
|
||||
return nil, fmt.Errorf("move database update failed: %v; filesystem rollback failed: %w", moveErr, rollbackErr)
|
||||
}
|
||||
return nil, moveErr
|
||||
}
|
||||
|
||||
updated, err := s.repo.GetDocumentByID(ctx, record.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.onChange != nil {
|
||||
s.onChange(DocumentChange{
|
||||
Type: "move",
|
||||
DocumentID: updated.ID,
|
||||
Path: updated.Path,
|
||||
OldPath: oldPath,
|
||||
Title: updated.Title,
|
||||
Hash: updated.CurrentHash,
|
||||
})
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoadPage(ctx context.Context, requestPath string) (*Page, error) {
|
||||
if _, err := s.SyncSourceDir(ctx); err != nil {
|
||||
return nil, err
|
||||
@@ -301,10 +405,10 @@ func (s *Service) resolveRecord(ctx context.Context, requestPath string) (*Docum
|
||||
lastErr error
|
||||
)
|
||||
for _, candidate := range normalizeRequestPathCandidates(requestPath) {
|
||||
found, err := s.repo.GetDocumentByPath(ctx, candidate)
|
||||
found, err := s.repo.GetDocumentByPathOrAlias(ctx, candidate)
|
||||
if err == nil {
|
||||
record = found
|
||||
normalized = candidate
|
||||
normalized = found.Path
|
||||
break
|
||||
}
|
||||
lastErr = err
|
||||
@@ -342,6 +446,10 @@ func normalizeWritableDocumentPath(path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func NormalizeDocumentPath(path string) (string, error) {
|
||||
return normalizeWritableDocumentPath(path)
|
||||
}
|
||||
|
||||
func (s *Service) syncFile(ctx context.Context, path string) (*DocumentChange, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -400,6 +508,7 @@ func (s *Service) syncFile(ctx context.Context, path string) (*DocumentChange, e
|
||||
|
||||
s.logger.Debug("synced document", "path", relative, "hash", record.Hash)
|
||||
return &DocumentChange{
|
||||
DocumentID: documentID,
|
||||
Path: relative,
|
||||
Title: rendered.Title,
|
||||
Hash: record.Hash,
|
||||
|
||||
Reference in New Issue
Block a user