diff --git a/api/e/codes.go b/api/e/codes.go index d7036202b..a8601bb5c 100644 --- a/api/e/codes.go +++ b/api/e/codes.go @@ -4,6 +4,7 @@ var Codes = map[string]int{ "NOT_AUTH": 20001, "REQUEST_BODY_ERROR": 40001, + "UPLOAD_FILE_ERROR": 40002, "DATABASE_ERROR": 50001, } diff --git a/api/memo.go b/api/memo.go index a3793f66d..375a7fffe 100644 --- a/api/memo.go +++ b/api/memo.go @@ -88,7 +88,7 @@ func handleDeleteMemo(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) memoId := vars["id"] - _, err := store.DeleteMemo(memoId) + err := store.DeleteMemo(memoId) if err != nil { e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) diff --git a/api/query.go b/api/query.go index 5b2cfb774..8e35df675 100644 --- a/api/query.go +++ b/api/query.go @@ -86,7 +86,7 @@ func handleDeleteQuery(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) queryId := vars["id"] - _, err := store.DeleteQuery(queryId) + err := store.DeleteQuery(queryId) if err != nil { e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) diff --git a/api/resource.go b/api/resource.go new file mode 100644 index 000000000..5cc26bb05 --- /dev/null +++ b/api/resource.go @@ -0,0 +1,114 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "memos/api/e" + "memos/store" + "net/http" + + "github.com/gorilla/mux" +) + +func handleGetMyResources(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInSession(r) + + resources, err := store.GetResourcesByUserId(userId) + + if err != nil { + e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) + return + } + + json.NewEncoder(w).Encode(Response{ + Succeed: true, + Message: "", + Data: resources, + }) +} + +func handleUploadResource(w http.ResponseWriter, r *http.Request) { + userId, _ := GetUserIdInSession(r) + + r.ParseMultipartForm(10 << 20) + + file, handler, err := r.FormFile("file") + + if err != nil { + e.ErrorHandler(w, "REQUEST_BODY_ERROR", "Bad request") + return + } + + defer file.Close() + + filename := handler.Filename + filetype := handler.Header.Get("Content-Type") + size := handler.Size + + fileBytes, err := ioutil.ReadAll(file) + + if err != nil { + e.ErrorHandler(w, "UPLOAD_FILE_ERROR", "Read file error") + fmt.Println(err) + } + + resource, err := store.CreateResource(userId, filename, fileBytes, filetype, size) + + if err != nil { + e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) + return + } + + json.NewEncoder(w).Encode(Response{ + Succeed: true, + Message: "Upload file succeed", + Data: resource, + }) +} + +func handleDeleteResource(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + resourceId := vars["id"] + + err := store.DeleteResourceById(resourceId) + + if err != nil { + e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) + return + } + + json.NewEncoder(w).Encode(Response{ + Succeed: true, + Message: "", + Data: nil, + }) +} + +func handleGetResource(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + resourceId := vars["id"] + filename := vars["filename"] + + resource, err := store.GetResourceByIdAndFilename(resourceId, filename) + + if err != nil { + e.ErrorHandler(w, "DATABASE_ERROR", err.Error()) + return + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(resource.Blob) +} + +func RegisterResourceRoutes(r *mux.Router) { + resourceRouter := r.PathPrefix("/").Subrouter() + + resourceRouter.Use(AuthCheckerMiddleWare) + + resourceRouter.HandleFunc("/api/resource/all", handleGetMyResources).Methods("GET") + resourceRouter.HandleFunc("/api/resource/", handleUploadResource).Methods("PUT") + resourceRouter.HandleFunc("/api/resource/{id}", handleDeleteResource).Methods("DELETE") + resourceRouter.HandleFunc("/r/{id}/{filename}", handleGetResource).Methods("GET") +} diff --git a/resources/initial_db.sql b/resources/initial_db.sql index 3fa91dcc1..3ed854d54 100644 --- a/resources/initial_db.sql +++ b/resources/initial_db.sql @@ -1,5 +1,6 @@ DROP TABLE IF EXISTS `memos`; DROP TABLE IF EXISTS `queries`; +DROP TABLE IF EXISTS `resources`; DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( @@ -33,6 +34,18 @@ CREATE TABLE `memos` ( FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) ); +CREATE TABLE `resources` ( + `id` TEXT NOT NULL PRIMARY KEY, + `user_id` TEXT NOT NULL, + `filename` TEXT NOT NULL, + `blob` BLOB NOT NULL, + `type` TEXT NOT NULL, + `size` INTEGER NOT NULL DEFAULT 0, + `created_at` TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')), + FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) +); + + INSERT INTO `users` (`id`, `username`, `password`) VALUES diff --git a/resources/memos.db b/resources/memos.db index 9af4c3669..f3240385c 100644 Binary files a/resources/memos.db and b/resources/memos.db differ diff --git a/server/main.go b/server/main.go index ddcca0fad..34f3332cc 100644 --- a/server/main.go +++ b/server/main.go @@ -17,6 +17,7 @@ func main() { api.RegisterUserRoutes(r) api.RegisterMemoRoutes(r) api.RegisterQueryRoutes(r) + api.RegisterResourceRoutes(r) webServe := api.SPAHandler{ StaticPath: "./web/dist", diff --git a/store/memo.go b/store/memo.go index 64acc4f95..348b6c0fb 100644 --- a/store/memo.go +++ b/store/memo.go @@ -57,11 +57,11 @@ func UpdateMemo(id string, memoPatch *MemoPatch) (Memo, error) { return memo, err } -func DeleteMemo(memoId string) (error, error) { +func DeleteMemo(memoId string) error { query := `DELETE FROM memos WHERE id=?` _, err := DB.Exec(query, memoId) - return nil, err + return err } func GetMemoById(id string) (Memo, error) { diff --git a/store/query.go b/store/query.go index 7a8aab1ac..b68382311 100644 --- a/store/query.go +++ b/store/query.go @@ -64,11 +64,11 @@ func UpdateQuery(id string, queryPatch *QueryPatch) (Query, error) { return query, err } -func DeleteQuery(queryId string) (error, error) { +func DeleteQuery(queryId string) error { query := `DELETE FROM queries WHERE id=?` _, err := DB.Exec(query, queryId) - return nil, err + return err } func GetQueryById(queryId string) (Query, error) { diff --git a/store/resource.go b/store/resource.go new file mode 100644 index 000000000..ec8a46558 --- /dev/null +++ b/store/resource.go @@ -0,0 +1,63 @@ +package store + +import "memos/utils" + +type Resource struct { + Id string `json:"id"` + UserId string `json:"userId"` + Filename string `json:"filename"` + Blob []byte `json:"blob"` + Type string `json:"type"` + Size int64 `json:"size"` + CreatedAt string `json:"createdAt"` +} + +func CreateResource(userId string, filename string, blob []byte, filetype string, size int64) (Resource, error) { + newResource := Resource{ + Id: utils.GenUUID(), + UserId: userId, + Filename: filename, + Blob: blob, + Type: filetype, + Size: size, + CreatedAt: utils.GetNowDateTimeStr(), + } + + query := `INSERT INTO resources (id, user_id, filename, blob, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)` + _, err := DB.Exec(query, newResource.Id, newResource.UserId, newResource.Filename, newResource.Blob, newResource.Type, newResource.Size, newResource.CreatedAt) + + return newResource, err +} + +func GetResourcesByUserId(userId string) ([]Resource, error) { + query := `SELECT id, filename, type, size, created_at FROM resources WHERE user_id=?` + rows, _ := DB.Query(query, userId) + defer rows.Close() + + resources := []Resource{} + + for rows.Next() { + resource := Resource{} + rows.Scan(&resource.Id, &resource.Filename, &resource.Type, &resource.Size, &resource.CreatedAt) + resources = append(resources, resource) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return resources, nil +} + +func GetResourceByIdAndFilename(id string, filename string) (Resource, error) { + query := `SELECT id, filename, blob, type, size FROM resources WHERE id=? AND filename=?` + resource := Resource{} + err := DB.QueryRow(query, id, filename).Scan(&resource.Id, &resource.Filename, &resource.Blob, &resource.Type, &resource.Size) + return resource, err +} + +func DeleteResourceById(id string) error { + query := `DELETE FROM resources WHERE id=?` + _, err := DB.Exec(query, id) + return err +} diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 372b48abb..529b98877 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -6,16 +6,26 @@ type ResponseType = { data: T; }; -async function request(method: string, url: string, data?: any): Promise> { +type RequestConfig = { + method: string; + url: string; + data?: any; + dataType?: "json" | "file"; +}; + +async function request(config: RequestConfig): Promise> { + const { method, url, data, dataType } = config; const requestConfig: RequestInit = { method, }; - if (method !== "GET") { - requestConfig.headers = { - "Content-Type": "application/json", - }; - if (data !== null) { + if (data !== undefined) { + if (dataType === "file") { + requestConfig.body = data; + } else { + requestConfig.headers = { + "Content-Type": "application/json", + }; requestConfig.body = JSON.stringify(data); } } @@ -32,87 +42,169 @@ async function request(method: string, url: string, data?: any): Promise("GET", "/api/user/me"); + return request({ + method: "GET", + url: "/api/user/me", + }); } export function signin(username: string, password: string) { - return request("POST", "/api/auth/signin", { username, password }); + return request({ + method: "POST", + url: "/api/auth/signin", + data: { username, password }, + }); } export function signup(username: string, password: string) { - return request("POST", "/api/auth/signup", { username, password }); + return request({ + method: "POST", + url: "/api/auth/signup", + data: { username, password }, + }); } export function signout() { - return request("POST", "/api/auth/signout"); + return request({ + method: "POST", + url: "/api/auth/signout", + }); } export function checkUsernameUsable(username: string) { - return request("POST", "/api/user/checkusername", { username }); + return request({ + method: "POST", + url: "/api/user/checkusername", + data: { username }, + }); } export function checkPasswordValid(password: string) { - return request("POST", "/api/user/validpassword", { password }); + return request({ + method: "POST", + url: "/api/user/validpassword", + data: { password }, + }); } export function updateUserinfo(userinfo: Partial<{ username: string; password: string; githubName: string }>) { - return request("PATCH", "/api/user/me", userinfo); + return request({ + method: "PATCH", + url: "/api/user/me", + data: userinfo, + }); } export function getMyMemos() { - return request("GET", "/api/memo/all"); + return request({ + method: "GET", + url: "/api/memo/all", + }); } export function getMyDeletedMemos() { - return request("GET", "/api/memo/all?deleted=true"); + return request({ + method: "GET", + url: "/api/memo/all?deleted=true", + }); } export function createMemo(content: string) { - return request("PUT", "/api/memo/", { content }); + return request({ + method: "PUT", + url: "/api/memo/", + data: { content }, + }); } export function updateMemo(memoId: string, content: string) { - return request("PATCH", `/api/memo/${memoId}`, { content }); + return request({ + method: "PATCH", + url: `/api/memo/${memoId}`, + data: { content }, + }); } export function hideMemo(memoId: string) { - return request("PATCH", `/api/memo/${memoId}`, { - deletedAt: utils.getDateTimeString(Date.now()), + return request({ + method: "PATCH", + url: `/api/memo/${memoId}`, + data: { + deletedAt: utils.getDateTimeString(Date.now()), + }, }); } export function restoreMemo(memoId: string) { - return request("PATCH", `/api/memo/${memoId}`, { - deletedAt: "", + return request({ + method: "PATCH", + url: `/api/memo/${memoId}`, + data: { + deletedAt: "", + }, }); } export function deleteMemo(memoId: string) { - return request("DELETE", `/api/memo/${memoId}`); + return request({ + method: "DELETE", + url: `/api/memo/${memoId}`, + }); } export function getMyQueries() { - return request("GET", "/api/query/all"); + return request({ + method: "GET", + url: "/api/query/all", + }); } export function createQuery(title: string, querystring: string) { - return request("PUT", "/api/query/", { title, querystring }); + return request({ + method: "PUT", + url: "/api/query/", + data: { title, querystring }, + }); } export function updateQuery(queryId: string, title: string, querystring: string) { - return request("PATCH", `/api/query/${queryId}`, { title, querystring }); + return request({ + method: "PATCH", + url: `/api/query/${queryId}`, + data: { title, querystring }, + }); } export function deleteQueryById(queryId: string) { - return request("DELETE", `/api/query/${queryId}`); + return request({ + method: "DELETE", + url: `/api/query/${queryId}`, + }); } export function pinQuery(queryId: string) { - return request("PATCH", `/api/query/${queryId}`, { pinnedAt: utils.getDateTimeString(Date.now()) }); + return request({ + method: "PATCH", + url: `/api/query/${queryId}`, + data: { pinnedAt: utils.getDateTimeString(Date.now()) }, + }); } export function unpinQuery(queryId: string) { - return request("PATCH", `/api/query/${queryId}`, { pinnedAt: "" }); + return request({ + method: "PATCH", + url: `/api/query/${queryId}`, + data: { pinnedAt: "" }, + }); + } + + export function uploadFile(formData: FormData) { + return request({ + method: "PUT", + url: "/api/resource/", + data: formData, + dataType: "file", + }); } }