feat: integrate @tailwindcss/typography and enhance folder picker functionality
- Added @tailwindcss/typography dependency to package.json and tailwind.config.js. - Updated FolderPicker.vue to include additional properties (isGitRepo, hasDocuments) for directories. - Enhanced Sidebar.vue with folder creation and document handling features, including drag-and-drop functionality. - Implemented createFolder and moveDocument mutations in gql.ts for managing folder and document operations. - Added logic to handle folder creation and document creation prompts in Sidebar.vue. - Updated the layout and interactions in Sidebar.vue to improve user experience.
This commit is contained in:
@@ -14,6 +14,9 @@ type Query {
|
||||
# List documents under an optional path prefix.
|
||||
documents(prefix: String): [DocumentMeta!]!
|
||||
|
||||
# List all relative folder paths in the repository.
|
||||
folders: [String!]!
|
||||
|
||||
# Commit history for a document.
|
||||
history(slug: String!): [CommitEntry!]!
|
||||
|
||||
@@ -25,6 +28,9 @@ type Query {
|
||||
|
||||
# Whether the current storage path has uncommitted files.
|
||||
repoStatus: RepoStatus!
|
||||
|
||||
# List images next to a document.
|
||||
images(slug: String!): [ImageFile!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -57,6 +63,15 @@ type Mutation {
|
||||
|
||||
# Commit all uncommitted files in the storage path.
|
||||
initCommit(message: String!): Boolean!
|
||||
|
||||
# Delete an image file.
|
||||
deleteImage(slug: String!, filename: String!): Boolean!
|
||||
|
||||
# Create a new folder (directory) at the specified path.
|
||||
createFolder(path: String!): Boolean!
|
||||
|
||||
# Move a document from one location to another.
|
||||
moveDocument(oldSlug: String!, newSlug: String!): Boolean!
|
||||
}
|
||||
|
||||
# ── Types ──────────────────────────────────────────────────────────────────────
|
||||
@@ -79,8 +94,10 @@ type LDAPConfig {
|
||||
}
|
||||
|
||||
type ServerDirectory {
|
||||
name: String!
|
||||
path: String!
|
||||
name: String!
|
||||
path: String!
|
||||
isGitRepo: Boolean!
|
||||
hasDocuments: Boolean!
|
||||
}
|
||||
|
||||
type Document {
|
||||
@@ -114,6 +131,12 @@ type RepoStatus {
|
||||
hasUncommitted: Boolean!
|
||||
}
|
||||
|
||||
type ImageFile {
|
||||
name: String!
|
||||
url: String!
|
||||
size: Int!
|
||||
}
|
||||
|
||||
# ── Inputs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
input SetupInput {
|
||||
|
||||
@@ -42,6 +42,8 @@ func NewServer(cfg *config.Config, configPath string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/graphql", s.handleGraphQL)
|
||||
mux.HandleFunc("/health", s.handleHealth)
|
||||
mux.HandleFunc("/api/upload", s.handleUpload)
|
||||
mux.HandleFunc("/media/", s.handleMedia)
|
||||
|
||||
uiDir := os.Getenv("UI_DIR")
|
||||
if uiDir == "" {
|
||||
@@ -185,6 +187,9 @@ func (s *Server) dispatchAuthenticated(
|
||||
case strings.Contains(q, "deleteDocument"):
|
||||
s.handleDeleteDocument(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "deleteImage"):
|
||||
s.handleDeleteImage(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "createUser"):
|
||||
s.handleCreateUser(w, req, sess)
|
||||
|
||||
@@ -218,6 +223,18 @@ func (s *Server) dispatchAuthenticated(
|
||||
case strings.Contains(q, "history"):
|
||||
s.handleHistory(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "images"):
|
||||
s.handleImages(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "createFolder"):
|
||||
s.handleCreateFolder(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "moveDocument"):
|
||||
s.handleMoveDocument(w, req, sess)
|
||||
|
||||
case strings.Contains(q, "folders"):
|
||||
s.handleFolders(w, sess)
|
||||
|
||||
case strings.Contains(q, "diff"):
|
||||
s.handleDiff(w, req, sess)
|
||||
|
||||
@@ -240,18 +257,24 @@ func (s *Server) handleServerDirectories(w http.ResponseWriter, req gqlRequest,
|
||||
// Only allow setup state without auth
|
||||
if cfg != nil {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reqPath, _ := req.Variables["path"].(string)
|
||||
if reqPath == "" {
|
||||
// Also try "p" just in case the frontend sends p
|
||||
if p, ok := req.Variables["p"].(string); ok && p != "" {
|
||||
reqPath = p
|
||||
}
|
||||
}
|
||||
if reqPath == "" {
|
||||
if runtime.GOOS == "windows" {
|
||||
reqPath = "C:\\"
|
||||
@@ -268,8 +291,10 @@ func (s *Server) handleServerDirectories(w http.ResponseWriter, req gqlRequest,
|
||||
parent := filepath.Dir(reqPath)
|
||||
if parent != reqPath && parent != "." {
|
||||
out = append(out, map[string]interface{}{
|
||||
"name": "..",
|
||||
"path": parent,
|
||||
"name": "..",
|
||||
"path": parent,
|
||||
"isGitRepo": false,
|
||||
"hasDocuments": false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -282,9 +307,26 @@ func (s *Server) handleServerDirectories(w http.ResponseWriter, req gqlRequest,
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
fullPath := filepath.Join(reqPath, entry.Name())
|
||||
isGit := false
|
||||
if _, err := os.Stat(filepath.Join(fullPath, ".git")); err == nil {
|
||||
isGit = true
|
||||
}
|
||||
|
||||
hasDocs := false
|
||||
childEntries, _ := os.ReadDir(fullPath)
|
||||
for _, child := range childEntries {
|
||||
if !child.IsDir() && (strings.HasSuffix(child.Name(), ".md") || strings.HasSuffix(child.Name(), ".adoc")) {
|
||||
hasDocs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, map[string]interface{}{
|
||||
"name": entry.Name(),
|
||||
"path": filepath.Join(reqPath, entry.Name()),
|
||||
"name": entry.Name(),
|
||||
"path": fullPath,
|
||||
"isGitRepo": isGit,
|
||||
"hasDocuments": hasDocs,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -299,27 +341,27 @@ func (s *Server) handleServerDirectories(w http.ResponseWriter, req gqlRequest,
|
||||
// ── Session helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
func (s *Server) sessionFromRequest(r *http.Request) *auth.Session {
|
||||
raw := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(raw, "Bearer ") {
|
||||
log.Printf("[auth] No Bearer token found in header")
|
||||
return nil
|
||||
}
|
||||
token := strings.TrimPrefix(raw, "Bearer ")
|
||||
raw := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(raw, "Bearer ") {
|
||||
log.Printf("[auth] No Bearer token found in header")
|
||||
return nil
|
||||
}
|
||||
token := strings.TrimPrefix(raw, "Bearer ")
|
||||
|
||||
s.mu.RLock()
|
||||
mgr := s.authMgr
|
||||
s.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
mgr := s.authMgr
|
||||
s.mu.RUnlock()
|
||||
|
||||
if mgr == nil {
|
||||
log.Printf("[auth] authmgr is nil")
|
||||
return nil
|
||||
}
|
||||
sess, err := mgr.Validate(token)
|
||||
if err != nil {
|
||||
log.Printf("[auth] Token validation failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return sess
|
||||
if mgr == nil {
|
||||
log.Printf("[auth] authmgr is nil")
|
||||
return nil
|
||||
}
|
||||
sess, err := mgr.Validate(token)
|
||||
if err != nil {
|
||||
log.Printf("[auth] Token validation failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
// ── Document handlers ─────────────────────────────────────────────────────────
|
||||
@@ -534,12 +576,12 @@ func (s *Server) handleLogin(w http.ResponseWriter, req gqlRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[auth] login: %s (%s)", username, role)
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"login": token,
|
||||
},
|
||||
})
|
||||
log.Printf("[auth] login: %s (%s)", username, role)
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"login": token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, sess *auth.Session) {
|
||||
@@ -559,15 +601,15 @@ func (s *Server) handleLogout(w http.ResponseWriter, sess *auth.Session) {
|
||||
|
||||
func (s *Server) handleUsers(w http.ResponseWriter, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
database := s.database
|
||||
@@ -600,15 +642,15 @@ func (s *Server) handleUsers(w http.ResponseWriter, sess *auth.Session) {
|
||||
|
||||
func (s *Server) handleCreateUser(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := req.Variables["u"].(string)
|
||||
password, _ := req.Variables["p"].(string)
|
||||
@@ -655,15 +697,15 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, req gqlRequest, sess *a
|
||||
|
||||
func (s *Server) handleDeleteUser(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := req.Variables["username"].(string)
|
||||
if username == "" {
|
||||
@@ -745,15 +787,15 @@ func (s *Server) handleChangePassword(w http.ResponseWriter, req gqlRequest, ses
|
||||
|
||||
func (s *Server) handleUpdateLdapConfig(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
cfg := s.cfg
|
||||
@@ -801,15 +843,15 @@ func (s *Server) handleUpdateLdapConfig(w http.ResponseWriter, req gqlRequest, s
|
||||
|
||||
func (s *Server) handleConfig(w http.ResponseWriter, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
cfg := s.cfg
|
||||
@@ -899,15 +941,15 @@ func (s *Server) handleInitCommit(w http.ResponseWriter, req gqlRequest, sess *a
|
||||
|
||||
func (s *Server) handleUpdateStoragePath(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
log.Printf("[unauth] session is nil")
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
if sess.Role != "admin" {
|
||||
log.Printf("[unauth] user %s is not admin, role is %q", sess.Username, sess.Role)
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
cfg := s.cfg
|
||||
@@ -1227,78 +1269,359 @@ func resolveDBPath(configPath string) string {
|
||||
return filepath.Join(dirOf(configPath), "archivum.db")
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (s *Server) handleHistory(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil { writeGQLError(w, "UNAUTHORIZED"); return }
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
if slug == "" { writeGQLError(w, "slug required"); return }
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
if slug == "" {
|
||||
writeGQLError(w, "slug required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
|
||||
if repo == nil {
|
||||
writeGQLError(w, "git not initialized")
|
||||
return
|
||||
}
|
||||
if repo == nil {
|
||||
writeGQLError(w, "git not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
history, err := repo.Log(slug + ".adoc")
|
||||
if err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
history, err := repo.Log(slug + ".adoc")
|
||||
if err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var out []map[string]interface{}
|
||||
for _, entry := range history {
|
||||
out = append(out, map[string]interface{}{
|
||||
"hash": entry.Hash,
|
||||
"author": entry.Author,
|
||||
"email": entry.Email,
|
||||
"date": entry.Date,
|
||||
"subject": entry.Subject,
|
||||
"added": entry.Added,
|
||||
"removed": entry.Removed,
|
||||
})
|
||||
}
|
||||
writeJSONObj(w, map[string]interface{}{ "data": map[string]interface{}{ "history": out } })
|
||||
var out []map[string]interface{}
|
||||
for _, entry := range history {
|
||||
out = append(out, map[string]interface{}{
|
||||
"hash": entry.Hash,
|
||||
"author": entry.Author,
|
||||
"email": entry.Email,
|
||||
"date": entry.Date,
|
||||
"subject": entry.Subject,
|
||||
"added": entry.Added,
|
||||
"removed": entry.Removed,
|
||||
})
|
||||
}
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"history": out}})
|
||||
}
|
||||
|
||||
func (s *Server) handleDiff(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil { writeGQLError(w, "UNAUTHORIZED"); return }
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
fromHash, _ := req.Variables["fromHash"].(string)
|
||||
toHash, _ := req.Variables["toHash"].(string)
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
fromHash, _ := req.Variables["fromHash"].(string)
|
||||
toHash, _ := req.Variables["toHash"].(string)
|
||||
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
|
||||
if repo == nil { writeGQLError(w, "git not initialized"); return }
|
||||
if repo == nil {
|
||||
writeGQLError(w, "git not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
diff, err := repo.Diff(slug + ".adoc", fromHash, toHash)
|
||||
if err != nil { writeGQLError(w, err.Error()); return }
|
||||
diff, err := repo.Diff(slug+".adoc", fromHash, toHash)
|
||||
if err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{ "data": map[string]interface{}{ "diff": diff } })
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"diff": diff}})
|
||||
}
|
||||
|
||||
func (s *Server) handleDocumentAtCommit(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil { writeGQLError(w, "UNAUTHORIZED"); return }
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
hash, _ := req.Variables["hash"].(string)
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
hash, _ := req.Variables["hash"].(string)
|
||||
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
s.mu.RLock()
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
|
||||
if repo == nil { writeGQLError(w, "git not initialized"); return }
|
||||
if repo == nil {
|
||||
writeGQLError(w, "git not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
content, err := repo.Show(hash, slug + ".adoc")
|
||||
if err != nil { writeGQLError(w, err.Error()); return }
|
||||
content, err := repo.Show(hash, slug+".adoc")
|
||||
if err != nil {
|
||||
writeGQLError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{ "data": map[string]interface{}{ "documentAtCommit": content } })
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"documentAtCommit": content}})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateFolder(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
path, _ := req.Variables["path"].(string)
|
||||
if path == "" {
|
||||
writeGQLError(w, "path is required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialised")
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.CreateFolder(path); err != nil {
|
||||
log.Printf("[storage] failed to create folder %q: %v", path, err)
|
||||
writeGQLError(w, fmt.Sprintf("failed to create folder: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[storage] folder created at %q by %s", path, sess.Username)
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"createFolder": true}})
|
||||
}
|
||||
|
||||
func (s *Server) handleMoveDocument(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
oldSlug, _ := req.Variables["oldSlug"].(string)
|
||||
newSlug, _ := req.Variables["newSlug"].(string)
|
||||
|
||||
if oldSlug == "" {
|
||||
writeGQLError(w, "oldSlug is required")
|
||||
return
|
||||
}
|
||||
if newSlug == "" {
|
||||
writeGQLError(w, "newSlug is required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
repo := s.gitRepo
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialised")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if repo != nil {
|
||||
// Attempt to use git mv
|
||||
dir := filepath.ToSlash(filepath.Dir(newSlug))
|
||||
if dir != "." && dir != "" {
|
||||
_ = store.CreateFolder(dir)
|
||||
}
|
||||
err = repo.Move(oldSlug+".adoc", newSlug+".adoc", "Move "+oldSlug+" to "+newSlug, sess.Username, "")
|
||||
if err != nil {
|
||||
err = store.MoveDocument(oldSlug, newSlug)
|
||||
}
|
||||
} else {
|
||||
err = store.MoveDocument(oldSlug, newSlug)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[storage] failed to move document from %q to %q: %v", oldSlug, newSlug, err)
|
||||
writeGQLError(w, fmt.Sprintf("failed to move document: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[storage] document moved from %q to %q by %s", oldSlug, newSlug, sess.Username)
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"moveDocument": true}})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s.mu.RLock()
|
||||
am := s.authMgr
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Check auth
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if token == "" || am == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sess, errLogin := am.Validate(token)
|
||||
if errLogin != nil || sess == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
slug := r.FormValue("slug")
|
||||
if slug == "" {
|
||||
http.Error(w, "Missing slug", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("image")
|
||||
if err != nil {
|
||||
http.Error(w, "Missing image", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.SaveImage(slug, header.Filename, data); err != nil {
|
||||
http.Error(w, "Failed to save image", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate url
|
||||
relPath := strings.TrimPrefix(filepath.ToSlash(filepath.Join(filepath.Dir(slug), header.Filename)), "/")
|
||||
url := "/media/" + relPath
|
||||
|
||||
resp := map[string]interface{}{"url": url, "name": header.Filename, "size": len(data)}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleMedia(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
http.Error(w, "Storage not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
relPath := strings.TrimPrefix(r.URL.Path, "/media/")
|
||||
fullPath, err := store.GetImagePath(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, fullPath)
|
||||
}
|
||||
func (s *Server) handleFolders(w http.ResponseWriter, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialised")
|
||||
return
|
||||
}
|
||||
|
||||
folders, err := store.ListFolders()
|
||||
if err != nil {
|
||||
log.Printf("[folders] failed to list folders: %v", err)
|
||||
writeGQLError(w, fmt.Sprintf("failed to list folders: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[folders] user %q listed folders (found %d folders)", sess.Username, len(folders))
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"folders": folders,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleImages(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
if slug == "" {
|
||||
writeGQLError(w, "slug is required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
images, err := store.ListImages(slug)
|
||||
if err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to list images: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONObj(w, map[string]interface{}{"data": map[string]interface{}{"images": images}})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteImage(w http.ResponseWriter, req gqlRequest, sess *auth.Session) {
|
||||
if sess == nil {
|
||||
writeGQLError(w, "UNAUTHORIZED")
|
||||
return
|
||||
}
|
||||
|
||||
slug, _ := req.Variables["slug"].(string)
|
||||
filename, _ := req.Variables["filename"].(string)
|
||||
|
||||
if slug == "" || filename == "" {
|
||||
writeGQLError(w, "slug and filename are required")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
store := s.store
|
||||
s.mu.RUnlock()
|
||||
|
||||
if store == nil {
|
||||
writeGQLError(w, "storage not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeleteImage(slug, filename); err != nil {
|
||||
writeGQLError(w, fmt.Sprintf("failed to delete image: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[storage] deleted image %q from %q by %s", filename, slug, sess.Username)
|
||||
writeJSON(w, `{"data":{"deleteImage":true}}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user