admin.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /*
  2. * Rufs - Remote Union File System
  3. *
  4. * Copyright 2017 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the MIT
  7. * License, If a copy of the MIT License was not distributed with this
  8. * file, You can obtain one at https://opensource.org/licenses/MIT.
  9. */
  10. /*
  11. Package v1 contains Rufs REST API Version 1.
  12. Admin control endpoint
  13. /admin
  14. The admin endpoint can be used for various admin tasks such as registering
  15. new branches or mounting known branches.
  16. A GET request to the admin endpoint returns the current tree
  17. configuration; an object of all known branches and the current mapping:
  18. {
  19. branches : [ <known branches> ],
  20. tree : [ <current mapping> ]
  21. }
  22. A POST request to the admin endpoint creates a new tree. The body of
  23. the request should have the following form:
  24. "<name>"
  25. /admin/<tree>
  26. A DELETE request to a particular tree will delete the tree.
  27. /admin/<tree>/branch
  28. A new branch can be created in an existing tree by sending a POST request
  29. to the branch endpoint. The body of the request should have the following
  30. form:
  31. {
  32. branch : <Name of the branch>,
  33. rpc : <RPC definition of the remote branch (e.g. localhost:9020)>,
  34. fingerprint : <Expected SSL fingerprint of the remote branch or an empty string>
  35. }
  36. /admin/<tree>/mapping
  37. A new mapping can be created in an existing tree by sending a POST request
  38. to the mapping endpoint. The body of the request should have the following
  39. form:
  40. {
  41. branch : <Name of the branch>,
  42. dir : <Tree directory of the branch root>,
  43. writable : <Flag if the branch should handle write operations>
  44. }
  45. Dir listing endpoing
  46. /dir/<tree>/<path>
  47. The dir endpoing handles requests for the directory listing of a certain
  48. path. A request url should be of the following form:
  49. /dir/<tree>/<path>?recursive=<flag>&checksums=<flag>
  50. The request can optionally include the flag parameters (value should
  51. be 1 or 0) recursive and checksums. The recursive flag will add all
  52. subdirectories to the listing and the checksums flag will add
  53. checksums for all listed files.
  54. File queries and manipulation
  55. /file/{tree}/{path}
  56. A GET request to a specific file will return its contents. A POST will
  57. upload a new or overwrite an existing file. A DELETE request will delete
  58. an existing file.
  59. New files are expected to be uploaded using a multipart/form-data request.
  60. When uploading a new file the form field for the file should be named
  61. "uploadfile". The form can optionally contain a redirect field which
  62. will issue a redirect once the file has been uploaded.
  63. A PUT request is used to perform a file operation. The request body
  64. should be a JSON object of the form (parameters are operation specific):
  65. {
  66. action : <Action to perform>,
  67. files : <List of (full path) files which should be copied / renamed>
  68. newname : <New name of file (when renaming)>,
  69. newnames : <List of new file names when renaming multiple files using
  70. the files parameter>,
  71. destination : <Destination file when copying a single file - Destination
  72. directory when copying multiple files using the files
  73. parameter or syncing directories>
  74. }
  75. The action can either be: sync, rename, mkdir or copy. Copy and sync returns a JSON
  76. structure containing a progress id:
  77. {
  78. progress_id : <Id for progress of the copy operation>
  79. }
  80. Progress information
  81. /progress/<progress id>
  82. A GET request to the progress endpoint returns the current progress of
  83. an ongoing operation. The result should be:
  84. {
  85. "item": <Currently processing item>,
  86. "operation": <Name of operation>,
  87. "progress": <Current progress>,
  88. "subject": <Name of the subject on which the operation is performed>,
  89. "total_items": <Total number of items>,
  90. "total_progress": <Total progress>
  91. }
  92. Create zip files
  93. /zip/<tree>
  94. A post to the zip enpoint returns a zip file containing requested files. The
  95. files to include must be given as a list of file name with full path in the body.
  96. The body should be application/x-www-form-urlencoded encoded. The list should
  97. be a JSON encoded string as value of the value files. The body should have the
  98. following form:
  99. files=[ "<file1>", "<file2>" ]
  100. */
  101. package v1
  102. import (
  103. "encoding/json"
  104. "fmt"
  105. "net/http"
  106. "strconv"
  107. "devt.de/krotik/rufs"
  108. "devt.de/krotik/rufs/api"
  109. )
  110. /*
  111. EndpointAdmin is the mount endpoint URL (rooted). Handles everything
  112. under admin/...
  113. */
  114. const EndpointAdmin = api.APIRoot + APIv1 + "/admin/"
  115. /*
  116. AdminEndpointInst creates a new endpoint handler.
  117. */
  118. func AdminEndpointInst() api.RestEndpointHandler {
  119. return &adminEndpoint{}
  120. }
  121. /*
  122. Handler object for admin operations.
  123. */
  124. type adminEndpoint struct {
  125. *api.DefaultEndpointHandler
  126. }
  127. /*
  128. HandleGET handles an admin query REST call.
  129. */
  130. func (a *adminEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resources []string) {
  131. data := make(map[string]interface{})
  132. trees, err := api.Trees()
  133. if err != nil {
  134. http.Error(w, err.Error(), http.StatusBadRequest)
  135. return
  136. }
  137. refreshName := r.URL.Query().Get("refresh")
  138. for k, v := range trees {
  139. var tree map[string]interface{}
  140. if refreshName != "" && k == refreshName {
  141. v.Refresh()
  142. }
  143. json.Unmarshal([]byte(v.Config()), &tree)
  144. data[k] = tree
  145. }
  146. // Write data
  147. w.Header().Set("content-type", "application/json; charset=utf-8")
  148. json.NewEncoder(w).Encode(data)
  149. }
  150. /*
  151. HandlePOST handles REST calls to create a new tree.
  152. */
  153. func (a *adminEndpoint) HandlePOST(w http.ResponseWriter, r *http.Request, resources []string) {
  154. var tree *rufs.Tree
  155. var ok bool
  156. var err error
  157. var data map[string]interface{}
  158. if len(resources) == 0 {
  159. var name string
  160. if err := json.NewDecoder(r.Body).Decode(&name); err != nil {
  161. http.Error(w, fmt.Sprintf("Could not decode request body: %v", err.Error()),
  162. http.StatusBadRequest)
  163. return
  164. } else if name == "" {
  165. http.Error(w, fmt.Sprintf("Body must contain the tree name as a non-empty JSON string"),
  166. http.StatusBadRequest)
  167. return
  168. }
  169. // Create a new tree
  170. tree, err := rufs.NewTree(api.TreeConfigTemplate, api.TreeCertTemplate)
  171. if err != nil {
  172. http.Error(w, fmt.Sprintf("Could not create new tree: %v", err.Error()),
  173. http.StatusBadRequest)
  174. return
  175. }
  176. // Store the new tree
  177. if err := api.AddTree(name, tree); err != nil {
  178. http.Error(w, fmt.Sprintf("Could not add new tree: %v", err.Error()),
  179. http.StatusBadRequest)
  180. }
  181. return
  182. }
  183. if !checkResources(w, resources, 2, 2, "Need a tree name and a section (either branches or mapping)") {
  184. return
  185. }
  186. if tree, ok, err = api.GetTree(resources[0]); err == nil && !ok {
  187. err = fmt.Errorf("Unknown tree: %v", resources[0])
  188. }
  189. if err != nil {
  190. http.Error(w, err.Error(), http.StatusBadRequest)
  191. return
  192. }
  193. if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
  194. http.Error(w, fmt.Sprintf("Could not decode request body: %v", err.Error()),
  195. http.StatusBadRequest)
  196. return
  197. }
  198. if resources[1] == "branch" {
  199. // Add a new branch
  200. if rpc, ok := getMapValue(w, data, "rpc"); ok {
  201. if branch, ok := getMapValue(w, data, "branch"); ok {
  202. if fingerprint, ok := getMapValue(w, data, "fingerprint"); ok {
  203. if err := tree.AddBranch(branch, rpc, fingerprint); err != nil {
  204. http.Error(w, fmt.Sprintf("Could not add branch: %v", err.Error()),
  205. http.StatusBadRequest)
  206. }
  207. }
  208. }
  209. }
  210. } else if resources[1] == "mapping" {
  211. // Add a new mapping
  212. if _, ok := data["dir"]; ok {
  213. if dir, ok := getMapValue(w, data, "dir"); ok {
  214. if branch, ok := getMapValue(w, data, "branch"); ok {
  215. if writeableStr, ok := getMapValue(w, data, "writeable"); ok {
  216. writeable, err := strconv.ParseBool(writeableStr)
  217. if err != nil {
  218. http.Error(w, fmt.Sprintf("Writeable value must be a boolean: %v", err.Error()),
  219. http.StatusBadRequest)
  220. } else if err := tree.AddMapping(dir, branch, writeable); err != nil {
  221. http.Error(w, fmt.Sprintf("Could not add branch: %v", err.Error()),
  222. http.StatusBadRequest)
  223. }
  224. }
  225. }
  226. }
  227. }
  228. }
  229. }
  230. /*
  231. HandleDELETE handles REST calls to delete an existing tree.
  232. */
  233. func (a *adminEndpoint) HandleDELETE(w http.ResponseWriter, r *http.Request, resources []string) {
  234. if !checkResources(w, resources, 1, 1, "Need a tree name") {
  235. return
  236. }
  237. // Delete the tree
  238. if err := api.RemoveTree(resources[0]); err != nil {
  239. http.Error(w, fmt.Sprintf("Could not remove tree: %v", err.Error()),
  240. http.StatusBadRequest)
  241. }
  242. }
  243. /*
  244. SwaggerDefs is used to describe the endpoint in swagger.
  245. */
  246. func (a *adminEndpoint) SwaggerDefs(s map[string]interface{}) {
  247. s["paths"].(map[string]interface{})["/v1/admin"] = map[string]interface{}{
  248. "get": map[string]interface{}{
  249. "summary": "Return all current tree configurations.",
  250. "description": "All current tree configurations; each object has a list of all known branches and the current mapping.",
  251. "produces": []string{
  252. "text/plain",
  253. "application/json",
  254. },
  255. "parameters": []map[string]interface{}{
  256. {
  257. "name": "refresh",
  258. "in": "query",
  259. "description": "Refresh a particular tree (reload branches and mappings).",
  260. "required": false,
  261. "type": "string",
  262. },
  263. },
  264. "responses": map[string]interface{}{
  265. "200": map[string]interface{}{
  266. "description": "A key-value map of tree name to tree configuration",
  267. },
  268. "default": map[string]interface{}{
  269. "description": "Error response",
  270. "schema": map[string]interface{}{
  271. "$ref": "#/definitions/Error",
  272. },
  273. },
  274. },
  275. },
  276. "post": map[string]interface{}{
  277. "summary": "Create a new tree.",
  278. "description": "Create a new named tree.",
  279. "consumes": []string{
  280. "application/json",
  281. },
  282. "produces": []string{
  283. "text/plain",
  284. },
  285. "parameters": []map[string]interface{}{
  286. {
  287. "name": "data",
  288. "in": "body",
  289. "description": "Name of the new tree.",
  290. "required": true,
  291. "schema": map[string]interface{}{
  292. "type": "string",
  293. },
  294. },
  295. },
  296. "responses": map[string]interface{}{
  297. "200": map[string]interface{}{
  298. "description": "Returns an empty body if successful.",
  299. },
  300. "default": map[string]interface{}{
  301. "description": "Error response",
  302. "schema": map[string]interface{}{
  303. "$ref": "#/definitions/Error",
  304. },
  305. },
  306. },
  307. },
  308. }
  309. s["paths"].(map[string]interface{})["/v1/admin/{tree}"] = map[string]interface{}{
  310. "delete": map[string]interface{}{
  311. "summary": "Delete a tree.",
  312. "description": "Delete a named tree.",
  313. "produces": []string{
  314. "text/plain",
  315. },
  316. "parameters": []map[string]interface{}{
  317. {
  318. "name": "tree",
  319. "in": "path",
  320. "description": "Name of the tree.",
  321. "required": true,
  322. "type": "string",
  323. },
  324. },
  325. "responses": map[string]interface{}{
  326. "200": map[string]interface{}{
  327. "description": "Returns an empty body if successful.",
  328. },
  329. "default": map[string]interface{}{
  330. "description": "Error response",
  331. "schema": map[string]interface{}{
  332. "$ref": "#/definitions/Error",
  333. },
  334. },
  335. },
  336. },
  337. }
  338. s["paths"].(map[string]interface{})["/v1/admin/{tree}/branch"] = map[string]interface{}{
  339. "post": map[string]interface{}{
  340. "summary": "Add a new branch.",
  341. "description": "Add a new remote branch to the tree.",
  342. "consumes": []string{
  343. "application/json",
  344. },
  345. "produces": []string{
  346. "text/plain",
  347. },
  348. "parameters": []map[string]interface{}{
  349. {
  350. "name": "tree",
  351. "in": "path",
  352. "description": "Name of the tree.",
  353. "required": true,
  354. "type": "string",
  355. },
  356. {
  357. "name": "data",
  358. "in": "body",
  359. "description": "Definition of the new branch.",
  360. "required": true,
  361. "schema": map[string]interface{}{
  362. "type": "object",
  363. "properties": map[string]interface{}{
  364. "branch": map[string]interface{}{
  365. "description": "Name of the remote branch (must match on the remote branch).",
  366. "type": "string",
  367. },
  368. "rpc": map[string]interface{}{
  369. "description": "RPC definition of the remote branch (e.g. localhost:9020).",
  370. "type": "string",
  371. },
  372. "fingerprint": map[string]interface{}{
  373. "description": "Expected SSL fingerprint of the remote branch (shown during startup) or an empty string.",
  374. "type": "string",
  375. },
  376. },
  377. },
  378. },
  379. },
  380. "responses": map[string]interface{}{
  381. "200": map[string]interface{}{
  382. "description": "Returns an empty body if successful.",
  383. },
  384. "default": map[string]interface{}{
  385. "description": "Error response",
  386. "schema": map[string]interface{}{
  387. "$ref": "#/definitions/Error",
  388. },
  389. },
  390. },
  391. },
  392. }
  393. s["paths"].(map[string]interface{})["/v1/admin/{tree}/mapping"] = map[string]interface{}{
  394. "post": map[string]interface{}{
  395. "summary": "Add a new mapping.",
  396. "description": "Add a new mapping to the tree.",
  397. "consumes": []string{
  398. "application/json",
  399. },
  400. "produces": []string{
  401. "text/plain",
  402. },
  403. "parameters": []map[string]interface{}{
  404. {
  405. "name": "tree",
  406. "in": "path",
  407. "description": "Name of the tree.",
  408. "required": true,
  409. "type": "string",
  410. },
  411. {
  412. "name": "data",
  413. "in": "body",
  414. "description": "Definition of the new branch.",
  415. "required": true,
  416. "schema": map[string]interface{}{
  417. "type": "object",
  418. "properties": map[string]interface{}{
  419. "branch": map[string]interface{}{
  420. "description": "Name of the known remote branch.",
  421. "type": "string",
  422. },
  423. "dir": map[string]interface{}{
  424. "description": "Tree directory which should hold the branch root.",
  425. "type": "string",
  426. },
  427. "writable": map[string]interface{}{
  428. "description": "Flag if the branch should be mapped as writable.",
  429. "type": "string",
  430. },
  431. },
  432. },
  433. },
  434. },
  435. "responses": map[string]interface{}{
  436. "200": map[string]interface{}{
  437. "description": "Returns an empty body if successful.",
  438. },
  439. "default": map[string]interface{}{
  440. "description": "Error response",
  441. "schema": map[string]interface{}{
  442. "$ref": "#/definitions/Error",
  443. },
  444. },
  445. },
  446. },
  447. }
  448. // Add generic error object to definition
  449. s["definitions"].(map[string]interface{})["Error"] = map[string]interface{}{
  450. "description": "A human readable error mesage.",
  451. "type": "string",
  452. }
  453. }