focalboard/server/main/api.go
2020-10-16 11:41:56 +02:00

284 lines
7.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
)
// ----------------------------------------------------------------------------------------------------
// REST APIs
type API struct{}
func NewAPI() *API {
return &API{}
}
func (a *API) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/v1/blocks", a.handleGetBlocks).Methods("GET")
r.HandleFunc("/api/v1/blocks", a.handlePostBlocks).Methods("POST")
r.HandleFunc("/api/v1/blocks/{blockID}", a.handleDeleteBlock).Methods("DELETE")
r.HandleFunc("/api/v1/blocks/{blockID}/subtree", a.handleGetSubTree).Methods("GET")
r.HandleFunc("/api/v1/files", a.handleUploadFile).Methods("POST")
r.HandleFunc("/files/{filename}", a.handleServeFile).Methods("GET")
r.HandleFunc("/api/v1/blocks/export", a.handleExport).Methods("GET")
r.HandleFunc("/api/v1/blocks/import", a.handleImport).Methods("POST")
}
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
parentID := query.Get("parent_id")
blockType := query.Get("type")
var blocks []Block
if len(blockType) > 0 && len(parentID) > 0 {
blocks = store.getBlocksWithParentAndType(parentID, blockType)
} else if len(blockType) > 0 {
blocks = store.getBlocksWithType(blockType)
} else {
blocks = store.getBlocksWithParent(parentID)
}
log.Printf("GetBlocks parentID: %s, type: %s, %d result(s)", parentID, blockType, len(blocks))
json, err := json.Marshal(blocks)
if err != nil {
log.Printf(`ERROR json.Marshal: %v`, r)
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
jsonBytesResponse(w, http.StatusOK, json)
}
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
// Catch panics from parse errors, etc.
defer func() {
if r := recover(); r != nil {
log.Printf(`ERROR: %v`, r)
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
}()
var blocks []Block
err = json.Unmarshal([]byte(requestBody), &blocks)
if err != nil {
errorResponse(w, http.StatusInternalServerError, ``)
return
}
var blockIDsToNotify = []string{}
uniqueBlockIDs := make(map[string]bool)
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
errorResponse(w, http.StatusInternalServerError, fmt.Sprintf(`{"description": "missing type", "id": "%s"}`, block.ID))
return
}
if block.CreateAt < 1 {
errorResponse(w, http.StatusInternalServerError, fmt.Sprintf(`{"description": "invalid createAt", "id": "%s"}`, block.ID))
return
}
if block.UpdateAt < 1 {
errorResponse(w, http.StatusInternalServerError, fmt.Sprintf(`{"description": "invalid updateAt", "id": "%s"}`, block.ID))
return
}
if !uniqueBlockIDs[block.ID] {
blockIDsToNotify = append(blockIDsToNotify, block.ID)
}
if len(block.ParentID) > 0 && !uniqueBlockIDs[block.ParentID] {
blockIDsToNotify = append(blockIDsToNotify, block.ParentID)
}
store.insertBlock(block)
}
wsServer.broadcastBlockChangeToWebsocketClients(blockIDsToNotify)
log.Printf("POST Blocks %d block(s)", len(blocks))
jsonStringResponse(w, http.StatusOK, "{}")
}
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
blockID := vars["blockID"]
var blockIDsToNotify = []string{blockID}
parentID := store.getParentID(blockID)
if len(parentID) > 0 {
blockIDsToNotify = append(blockIDsToNotify, parentID)
}
store.deleteBlock(blockID)
wsServer.broadcastBlockChangeToWebsocketClients(blockIDsToNotify)
log.Printf("DELETE Block %s", blockID)
jsonStringResponse(w, http.StatusOK, "{}")
}
func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
blockID := vars["blockID"]
blocks := store.getSubTree(blockID)
log.Printf("GetSubTree blockID: %s, %d result(s)", blockID, len(blocks))
json, err := json.Marshal(blocks)
if err != nil {
log.Printf(`ERROR json.Marshal: %v`, r)
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
jsonBytesResponse(w, http.StatusOK, json)
}
func (a *API) handleExport(w http.ResponseWriter, r *http.Request) {
blocks := store.getAllBlocks()
log.Printf("EXPORT Blocks, %d result(s)", len(blocks))
json, err := json.Marshal(blocks)
if err != nil {
log.Printf(`ERROR json.Marshal: %v`, r)
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
jsonBytesResponse(w, http.StatusOK, json)
}
func (a *API) handleImport(w http.ResponseWriter, r *http.Request) {
requestBody, err := ioutil.ReadAll(r.Body)
if err != nil {
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
// Catch panics from parse errors, etc.
defer func() {
if r := recover(); r != nil {
log.Printf(`ERROR: %v`, r)
errorResponse(w, http.StatusInternalServerError, `{}`)
return
}
}()
var blocks []Block
err = json.Unmarshal([]byte(requestBody), &blocks)
if err != nil {
errorResponse(w, http.StatusInternalServerError, ``)
return
}
for _, block := range blocks {
store.insertBlock(block)
}
log.Printf("IMPORT Blocks %d block(s)", len(blocks))
jsonStringResponse(w, http.StatusOK, "{}")
}
// File upload
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
contentType := "image/jpg"
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == "png" {
contentType = "image/png"
}
w.Header().Set("Content-Type", contentType)
folderPath := config.FilesPath
filePath := filepath.Join(folderPath, filename)
http.ServeFile(w, r, filePath)
}
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
fmt.Println(`handleUploadFile`)
file, handle, err := r.FormFile("file")
if err != nil {
fmt.Fprintf(w, "%v", err)
return
}
defer file.Close()
log.Printf(`handleUploadFile, filename: %s`, handle.Filename)
saveFile(w, file, handle)
}
func saveFile(w http.ResponseWriter, file multipart.File, handle *multipart.FileHeader) {
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Fprintf(w, "%v", err)
return
}
// NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(handle.Filename))
if fileExtension == ".jpeg" {
fileExtension = ".jpg"
}
filename := fmt.Sprintf(`%s%s`, createGUID(), fileExtension)
folderPath := config.FilesPath
filePath := filepath.Join(folderPath, filename)
os.MkdirAll(folderPath, os.ModePerm)
err = ioutil.WriteFile(filePath, data, 0666)
if err != nil {
jsonStringResponse(w, http.StatusInternalServerError, `{}`)
return
}
url := fmt.Sprintf(`%s/files/%s`, config.ServerRoot, filename)
log.Printf(`saveFile, url: %s`, url)
json := fmt.Sprintf(`{ "url": "%s" }`, url)
jsonStringResponse(w, http.StatusOK, json)
}
// Response helpers
func jsonStringResponse(w http.ResponseWriter, code int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(json)
}
func errorResponse(w http.ResponseWriter, code int, message string) {
log.Printf("%d ERROR", code)
w.WriteHeader(code)
fmt.Fprint(w, message)
}