feature: move docs
This commit is contained in:
@@ -54,6 +54,7 @@ type documentData struct {
|
||||
|
||||
type documentEditData struct {
|
||||
Browser browserData
|
||||
ID string
|
||||
Title string
|
||||
Path string
|
||||
Tags []string
|
||||
@@ -209,6 +210,24 @@ func (s *Server) handleDocument(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderDocumentPage(w, r, pagePath)
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentByID(w http.ResponseWriter, r *http.Request) {
|
||||
record, err := s.repository.GetDocumentByID(r.Context(), chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
s.renderError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
canRead, err := s.canReadDocumentRecord(r, *record)
|
||||
if err != nil {
|
||||
s.renderError(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if !canRead {
|
||||
s.renderError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, documentPageURL(record.Path), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) handleContentAsset(w http.ResponseWriter, r *http.Request, assetPath string) {
|
||||
cleanPath := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(assetPath, "/")))
|
||||
if cleanPath == "." || strings.HasPrefix(cleanPath, ".."+string(filepath.Separator)) || filepath.IsAbs(cleanPath) {
|
||||
@@ -291,6 +310,10 @@ func (s *Server) renderDocumentEditor(w http.ResponseWriter, r *http.Request, pa
|
||||
s.renderError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
if !requestPathMatchesDocument(pagePath, page.Path) {
|
||||
http.Redirect(w, r, documentPageURL(page.Path)+"/edit", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := s.documents.ListDocuments(r.Context())
|
||||
if err != nil {
|
||||
@@ -311,6 +334,7 @@ func (s *Server) renderDocumentEditor(w http.ResponseWriter, r *http.Request, pa
|
||||
BodyTemplate: "document_edit_content",
|
||||
Data: documentEditData{
|
||||
Browser: browser,
|
||||
ID: record.ID,
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
Tags: page.Tags,
|
||||
@@ -346,6 +370,10 @@ func (s *Server) renderDocumentPage(w http.ResponseWriter, r *http.Request, page
|
||||
s.renderError(w, r, http.StatusNotFound, "document not found")
|
||||
return
|
||||
}
|
||||
if !requestPathMatchesDocument(pagePath, page.Path) {
|
||||
http.Redirect(w, r, documentPageURL(page.Path), http.StatusFound)
|
||||
return
|
||||
}
|
||||
canWrite, err := s.canWriteDocumentRecord(r, *record)
|
||||
if err != nil {
|
||||
s.renderError(w, r, http.StatusInternalServerError, err.Error())
|
||||
@@ -539,7 +567,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderError(w, r, http.StatusInternalServerError, "inspect existing documents")
|
||||
return
|
||||
}
|
||||
existing, err := s.repository.GetDocumentByPath(r.Context(), path)
|
||||
existing, err := s.repository.GetDocumentByPathOrAlias(r.Context(), path)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
s.renderError(w, r, http.StatusInternalServerError, "inspect existing document")
|
||||
return
|
||||
@@ -611,7 +639,7 @@ func (s *Server) availableDocumentPath(ctx context.Context, path string) (string
|
||||
base := strings.TrimSuffix(path, extension)
|
||||
for suffix := 2; ; suffix++ {
|
||||
candidate := fmt.Sprintf("%s-%d%s", base, suffix, extension)
|
||||
existing, err := s.repository.GetDocumentByPath(ctx, candidate)
|
||||
existing, err := s.repository.GetDocumentByPathOrAlias(ctx, candidate)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return candidate, nil
|
||||
}
|
||||
@@ -827,6 +855,7 @@ func (s *Server) handleDocuments(w http.ResponseWriter, r *http.Request) {
|
||||
results := make([]map[string]any, 0, len(records))
|
||||
for _, record := range records {
|
||||
results = append(results, map[string]any{
|
||||
"id": record.ID,
|
||||
"path": record.Path,
|
||||
"title": record.Title,
|
||||
"hash": record.CurrentHash,
|
||||
@@ -844,6 +873,8 @@ func (s *Server) handleDocumentMutation(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(pagePath, "/move"):
|
||||
s.handleDocumentMovePath(w, r, strings.TrimSuffix(pagePath, "/move"))
|
||||
case strings.HasSuffix(pagePath, "/archive"):
|
||||
s.handleDocumentArchivePath(w, r, strings.TrimSuffix(pagePath, "/archive"))
|
||||
case strings.HasSuffix(pagePath, "/restore"):
|
||||
@@ -853,6 +884,64 @@ func (s *Server) handleDocumentMutation(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentMovePath(w http.ResponseWriter, r *http.Request, pagePath string) {
|
||||
if pagePath == "" {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "document path is required"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
BaseHash string `json:"baseHash"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
destination, err := docs.NormalizeDocumentPath(req.Path)
|
||||
if err != nil {
|
||||
writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
record, err := s.documentRecordForRequestPath(r, pagePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
return
|
||||
}
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
canWrite, err := s.canWriteDocumentRecord(r, *record)
|
||||
if err != nil {
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !canWrite || !s.canWriteFolder(r, documentFolder(destination)) {
|
||||
writeJSONWithStatus(w, http.StatusForbidden, map[string]string{"error": "permission denied"})
|
||||
return
|
||||
}
|
||||
|
||||
moved, err := s.documents.MoveDocument(r.Context(), record.Path, destination, req.BaseHash)
|
||||
if err != nil {
|
||||
var conflict *docs.DocumentMoveConflictError
|
||||
switch {
|
||||
case errors.As(err, &conflict):
|
||||
writeJSONWithStatus(w, http.StatusConflict, map[string]string{"error": conflict.Error()})
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "document not found"})
|
||||
default:
|
||||
writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSONWithStatus(w, http.StatusOK, map[string]any{
|
||||
"status": "moved",
|
||||
"id": moved.ID,
|
||||
"path": moved.Path,
|
||||
"url": documentPageURL(moved.Path),
|
||||
})
|
||||
}
|
||||
|
||||
func unescapeDocumentRoutePath(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", nil
|
||||
@@ -1011,6 +1100,17 @@ func trimEditSuffix(path string) (string, bool) {
|
||||
return strings.TrimSuffix(path, "/edit"), true
|
||||
}
|
||||
|
||||
func requestPathMatchesDocument(requestPath, documentPath string) bool {
|
||||
requestPath = strings.Trim(requestPath, "/")
|
||||
if requestPath == "" {
|
||||
return documentPath == "index.md" || documentPath == "getting-started.md"
|
||||
}
|
||||
if strings.HasSuffix(requestPath, ".md") {
|
||||
return requestPath == documentPath
|
||||
}
|
||||
return requestPath+".md" == documentPath || requestPath+"/index.md" == documentPath
|
||||
}
|
||||
|
||||
func isContentAssetPath(path string) bool {
|
||||
extension := strings.ToLower(filepath.Ext(path))
|
||||
return extension != "" && extension != ".md"
|
||||
|
||||
Reference in New Issue
Block a user