Compare commits

...

436 Commits

Author SHA1 Message Date
boojack e3d76193b9 chore: update global css (#658) 2022-12-02 22:00:03 +08:00
boojack 07f0c3f052 chore: update global css (#657) 2022-12-02 21:34:43 +08:00
boojack a467a7c173 feat: upgrade dev version to 0.8.1 (#656)
* feat: upgrade version to `0.8.1`

* chore: update
2022-12-02 21:09:11 +08:00
boojack 14f9f29348 chore: update user setting appearance (#654) 2022-12-02 20:00:34 +08:00
EINDEX 5451fd2d2c feat: add a product of logseq plugin (#652) 2022-12-02 19:46:39 +08:00
hoi-lau f092771ea1 fix: resource-container overflow (#649) 2022-12-02 19:45:22 +08:00
boojack 7c6d7226f5 feat: update appearance selector (#645) 2022-12-01 20:57:19 +08:00
Stephen Zhou eaebc6dcef fix: apperance can not auto switch (#644) 2022-12-01 19:39:58 +08:00
boojack c5200ca31b feat: dark mode for dialogs (#643) 2022-11-30 20:34:16 +08:00
Tiefseemonster 1078132b12 fix: member menu dropdown position (#639)
* fix: member menu dropdown position

* chore: cleanup

* chore: cleanup
2022-11-30 20:18:39 +08:00
Stephen Zhou 6b058cd299 feat: save folding option with localstorage (#641)
* fix: change folding option need reload

* fix: floding option undefied
2022-11-30 19:13:55 +08:00
Wence b8f24af5ae feat: dynamic lazy loading route with simple loading page (#632)
* feat: dynamic loading route with simple loading page

* fix: lint fix

* Update web/src/less/loading.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-29 22:13:22 +00:00
boojack 6384f5af74 feat: dark mode for main pages (#637)
* feat: update dark mode styles for auth and explore page

* feat: dark mode for home page
2022-11-29 21:44:52 +08:00
Zeng1998 52038d26d2 chore: update i18n for validator message (#636) 2022-11-29 21:35:40 +08:00
boojack 55f37664ef chore: add theme file for joyui (#635) 2022-11-29 20:15:16 +08:00
Zeng1998 ab8e3473a1 feat: support resources reuse (#620)
* feat: support resource reuse

* update

* update

* update

* update
2022-11-29 19:07:20 +08:00
Zeng1998 eba23c4f6e fix: add validation for user information update (#633) 2022-11-29 19:03:29 +08:00
Zeng1998 00fe6d3862 chore: add joyui tooltip for resources dialog (#630) 2022-11-29 09:36:48 +08:00
Charles Chin 3646b8f5dd docs: update readme (#628) 2022-11-29 06:34:44 +08:00
boojack d40639bf8e chore: update readme me with contributors graph (#626) 2022-11-28 22:48:02 +08:00
Zeng1998 12b81781b9 feat: share memo dialog (#618)
* feat: new share dialog

* update

* update

* update

* update
2022-11-28 21:20:35 +08:00
Zeng1998 b67a37453d feat: member management enhancement (#617)
* feat: member management enhancement

* update

* update

* update

* update
2022-11-28 19:59:11 +08:00
boojack b04e001db1 fix: image url host missing (#623) 2022-11-28 19:52:03 +08:00
Stephen Zhou fbe7b604ef feat: dark mode support for memo detail (#604)
* feat: dark mode support for memo detail

* chore: update

* chore: update

* chore: update
2022-11-28 19:40:08 +08:00
Zeng1998 0402cb7b27 fix: no user settings returns when patch user (#622) 2022-11-28 19:38:44 +08:00
Tiefseemonster b72bfc9c24 fix: selector dropdown position in fullscreen mod (#619) 2022-11-28 19:36:42 +08:00
Zeng1998 40e92f9463 fix: change password max length validation (#616) 2022-11-28 19:33:14 +08:00
Zeng1998 f883dd9c1d feat: create user repeat password (#614)
* feat: create user repeat password

* update
2022-11-28 19:32:53 +08:00
Wujiao233 d8bf55efb2 fix: shoutcut tag filter handle mutiple tags (#608)
* fix: shoutcut tag filter handle mutiple tags

* not edit parser
2022-11-28 19:32:01 +08:00
Wujiao233 f982e83d0a fix: clear shortcut filter when delete this shortcut (#611) 2022-11-28 06:14:25 +08:00
Jasper Platenburg 3472a6db26 fix: password field visible (#609) 2022-11-27 21:53:10 +08:00
boojack c79e51a91b chore: update readme (#606) 2022-11-27 16:04:35 +08:00
boojack ce795a2a7d chore: show content image (#602) 2022-11-27 09:01:19 +08:00
boojack 045819c312 fix: initial database schema (#601) 2022-11-27 08:52:43 +08:00
Tiefseemonster 2fa01886da fix: tooltip overlaps a window border (#599) 2022-11-27 08:48:21 +08:00
Tiefseemonster dd7d322c47 chore: add .vscode to gitignore (#596) 2022-11-27 07:56:19 +08:00
Tiefseemonster dfe71f33c2 fix: search bar dropdown disappearing (#593) 2022-11-26 23:13:02 +08:00
boojack db1d223448 fix: apperance select (#585) 2022-11-26 17:45:16 +08:00
Zeng1998 54271c1598 chore: fix some typos (#587) 2022-11-26 06:23:29 +00:00
Zeng1998 1ee8ebc9e1 fix: collapse btn cursor style (#586) 2022-11-26 05:12:37 +00:00
Stephen Zhou 6e5537d131 feat: dark mode support for explore page (#584)
* feat: dark mode support for auth page

* chore: update

* feat: dark mode support for explore page (#583)

* fix: avoid white text

* fix: import order
2022-11-26 12:19:00 +08:00
Stephen Zhou 90c85103c3 feat: dark mode support for auth page (#569)
* feat: dark mode support for auth page

* chore: update
2022-11-26 11:20:22 +08:00
Zeng1998 2d5d734da4 chore: update i18n for account settings (#582) 2022-11-26 02:23:57 +00:00
Zeng1998 e1e5121dd7 fix: get markdown image from backend (#581) 2022-11-26 10:20:49 +08:00
boojack 85db6721de chore: disable metrics collector (#580) 2022-11-26 09:23:38 +08:00
boojack b511a7b634 fix: user patch with empty email (#578) 2022-11-25 22:34:24 +08:00
boojack 88c3b1ad0f feat: update prod version (#577) 2022-11-25 22:17:24 +08:00
boojack 9fe15a03b3 chore(revert): update test image platforms (#576)
Revert "chore: update test image platforms (#575)"

This reverts commit 500afffbcf.
2022-11-25 22:08:43 +08:00
boojack 500afffbcf chore: update test image platforms (#575)
chore: update test image plantforms
2022-11-25 22:06:40 +08:00
Zeng1998 5f3cade810 feat: highlight the searched text in memo content (#514)
* feat: highlight the searched text in memo content

* update

* update

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-25 21:59:21 +08:00
boojack 072851e3ba chore: update dialog style (#574)
* chore: update tests

* chore: update dialog styles
2022-11-25 21:55:45 +08:00
boojack e2cbea2022 chore: update action name (#573) 2022-11-25 21:53:36 +08:00
boojack 29880be283 chore: update tests (#572) 2022-11-25 21:51:53 +08:00
Tiefseemonster f4e2b7319a fix: make tooltip text no-selecting (#567)
* no selecting tooltip text

With a double click on button, you can get selection on tooltip text. That may be distracting a little bit.

* Update web/src/less/memo.less

Co-authored-by: Stephen Zhou <hi@hyoban.cc>

Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-25 20:32:59 +08:00
Zeng1998 ff0db82dc3 chore: empty query text (#566) 2022-11-25 20:09:42 +08:00
Stephen Zhou 2daf085ce4 chore: fix vscode settings name (#565) 2022-11-25 01:51:43 +00:00
Tiefseemonster 495f1f2041 chore: fix name field in paragraph parser obj (#564) 2022-11-25 01:47:17 +00:00
boojack 89179f78c2 chore: add SECURITY.md (#562) 2022-11-25 09:05:52 +08:00
boojack 7ad7461a92 chore: add test docker image (#559) 2022-11-24 20:54:22 +08:00
boojack 8cfcd201b0 feat: support display video (#558) 2022-11-24 20:22:54 +08:00
boojack abcd3cfafb feat: add Strikethrough syntax (#557)
feat: add `Strikethrough` rule
2022-11-24 20:05:51 +08:00
boojack 50d41c456b chore: change memo created time (#556) 2022-11-24 19:55:52 +08:00
Zeng1998 1d41d53723 fix: icon style (#555) 2022-11-24 18:44:47 +08:00
boojack 0dc003854f chore: fix typo (#551) 2022-11-24 10:03:00 +08:00
Zeng1998 a4b7a77016 fix: cursor style (#549) 2022-11-24 09:30:17 +08:00
boojack 939e836d1b chore: update readme (#548) 2022-11-24 08:45:36 +08:00
boojack a0b35f7aa9 feat: add French i18n item (#547)
feat: add French i18n
2022-11-24 08:42:38 +08:00
baptiste313 574e160a11 chore: addition of the French translation and small correction for the English (#546) 2022-11-24 08:03:16 +08:00
boojack 2042737004 feat: add username field (#544)
* feat: add username field

* chore: update
2022-11-23 22:27:21 +08:00
boojack a0667abec8 chore: update data initial requests (#538) 2022-11-22 23:45:11 +08:00
boojack 362306a9cb chore: update i18n for visibility (#537) 2022-11-22 22:59:54 +08:00
Zeng1998 5e069f79a2 feat: esc to cancel editing (#532) 2022-11-22 19:54:25 +08:00
Zeng1998 7c78fb064f fix: empty filter value (#529) 2022-11-22 11:31:37 +00:00
Zeng1998 11a7e897dc fix: update issue template (#530) 2022-11-22 19:30:04 +08:00
Yarden Shoham d8adc0afe9 feat: extend validation config max length (#523) 2022-11-22 18:21:12 +08:00
boojack 17c1b97d23 fix: tag regexp (#518)
* fix: tag regexp

* chore: update
2022-11-21 23:49:52 +08:00
boojack 5aee2f46ab chore: update issue template (#517) 2022-11-21 23:38:21 +08:00
boojack 013ded1e04 chore: code clean (#516) 2022-11-21 23:23:05 +08:00
Zeng1998 0d0f893b93 chore: update i18 for memo sharing (#513) 2022-11-21 19:59:19 +08:00
Zeng1998 d2a87a4ddb chore: bgcolor of memo share image (#508) 2022-11-21 18:38:32 +08:00
boojack b05a176490 chore: add license to README (#506) 2022-11-21 07:20:36 +08:00
boojack d149926a39 chore: update seed data (#507) 2022-11-21 07:17:28 +08:00
boojack 2d49e96a8a feat: get image blob in backend (#495)
* feat: get image blob in backend

* chore: update
2022-11-19 18:43:56 +08:00
boojack 9036bd478b fix: image scrollbar (#494) 2022-11-19 17:36:25 +08:00
boojack 477130aa85 chore: update db filesize access control (#493) 2022-11-19 17:07:40 +08:00
boojack 878e0eabc8 feat: add crawler plugin (#492)
* feat: add crawler plugin

* chore: update

* chore: go mod tidy

* chore: update
2022-11-19 16:58:55 +08:00
boojack 62f63d4af7 chore: update dependencies version (#491) 2022-11-19 14:51:28 +08:00
Zeng1998 1acf2f8b13 chore: update i18 for settings (#490) 2022-11-19 10:33:52 +08:00
boojack a4a5e539ed chore: update dev version (#489) 2022-11-19 09:57:54 +08:00
boojack a2831b37c4 feat: add database filesize in UI (#488) 2022-11-18 21:17:52 +08:00
boojack 706b1b428f chore: add toast to system settings (#486) 2022-11-17 21:37:57 +08:00
boojack 1690566413 chore: update emoji picker (#483) 2022-11-17 21:11:58 +08:00
boojack a24b885236 chore: update share memo image (#482) 2022-11-17 21:01:26 +08:00
Stephen Zhou c89a6665b6 feat: fold memos according to horizontal rule (#478)
* feat: fold memos according to horizontal rule

* fix: button and content

* chore: update
2022-11-15 14:49:08 +00:00
Stephen Zhou 797accbc2c feat: parser for horizontal rule (#477)
* feat: parser for horizontal rule

* chore: revert
2022-11-15 21:56:44 +08:00
boojack 66f9bc48bb chore: remove mark memo (#476) 2022-11-15 21:22:08 +08:00
boojack de2eff474c chore: remove memo card dialog (#475) 2022-11-15 21:16:53 +08:00
Mahoo Huang 67195859dc fix: abnormal link regex (#474) 2022-11-15 21:15:10 +08:00
boojack 61abc6dd11 feat: update sharing memo image (#473) 2022-11-15 21:02:55 +08:00
boojack 9fe9245727 chore: update cancel edit button style (#472) 2022-11-15 20:35:56 +08:00
Zeng1998 906ec7994e fix: clear query text (#469)
* fix: clear query text

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-15 19:52:16 +08:00
Zeng1998 5258b0a5b4 chore: change memo card's bg color (#471) 2022-11-15 18:55:11 +08:00
boojack ceac53b6d0 feat: additional script system setting (#467) 2022-11-14 22:21:19 +08:00
Stephen Zhou 3775d5c9c2 feat: folding options (#459)
* feat: folding options

* chore: update

* chore: update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-14 22:06:05 +08:00
Zeng1998 da80d4ef62 fix: duration query string (#465) 2022-11-14 18:57:43 +08:00
Stephen Zhou 205ad0fd6d chore: change rss item title (#464) 2022-11-14 13:28:50 +08:00
Zeng1998 d208731f5f feat: visibility click filter (#463)
* feat: visibility click filter

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-13 21:23:40 +08:00
Stephen Zhou a90acdabb3 fix: route confusion entering from non-home page (#460)
Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-13 19:40:16 +08:00
Zeng1998 407d1cdcaa feat: add visibility filter (#461)
* feat: add visibility filter

* update
2022-11-13 19:34:22 +08:00
boojack cb50bbc3cb refactor: remove mixin colors (#458) 2022-11-13 14:25:02 +08:00
boojack 2743268fd7 chore: remove unused visibility selector (#457) 2022-11-13 10:47:21 +08:00
Zeng1998 8cc0977a01 fix: image url extraction (#453)
fix: image-url-extraction

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-12 21:25:50 +08:00
boojack 0809ec8c72 chore: update editor style (#456) 2022-11-12 21:22:51 +08:00
boojack db93710f85 chore: remove mobile editor style user setting (#455) 2022-11-12 21:01:34 +08:00
boojack 241c93c6b7 feat: update editor style (#454)
* feat: update editor style

* chore: update bg
2022-11-12 20:57:08 +08:00
boojack bf07ab9e2f fix: remove duplicate tag (#450)
fix: remove dumplicate tag
2022-11-12 11:31:16 +08:00
boojack fe05e6a464 chore: update version 0.7.2 (#447) 2022-11-12 09:19:44 +08:00
boojack d1aa6aa7f8 fix: editor resource list (#445) 2022-11-12 09:04:26 +08:00
boojack 79af7e8abf fix: parse tag list (#446) 2022-11-12 09:02:44 +08:00
boojack a142d975d7 feat: additional style system setting (#444)
* feat: additional style system setting

* feat: remove editor font setting
2022-11-11 23:42:44 +08:00
boojack 67691d1e99 feat: update visibility selector style (#441) 2022-11-11 19:25:21 +08:00
Zeng1998 9b827b4801 feat: add support for time-shortcut (#434)
* feat: add support for time-shortcut

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-11 19:14:38 +08:00
Zeng1998 421f4dbf60 feat: select visibility in editor (#421)
* feat: editing visibility selection

* update

* update

* update variable name

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-10 13:43:49 +00:00
boojack 8f119c4265 chore: update session key (#440) 2022-11-10 20:51:37 +08:00
boojack 9866702850 fix: parser regexp for special character (#439) 2022-11-10 20:38:14 +08:00
Yunliang Zhou a313a9bb31 fix: route confusion entering from non-home page (#430) 2022-11-10 19:50:12 +08:00
boojack e53f5fdd29 chore: update seed data (#437) 2022-11-10 08:41:11 +08:00
Zeng1998 b6c0a04394 fix: share memo with resource list (#431)
* fix: share memo with resource list

* update

* Update web/src/components/ShareMemoImageDialog.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-09 14:58:16 +00:00
Zeng1998 1e3b8315a0 chore: update MemoResources props (#432) 2022-11-09 22:10:31 +08:00
boojack dc5d705f8c feat: vacuum records manually (#420) 2022-11-06 04:21:58 +00:00
Zeng1998 4f10c12092 chore: update translation (#417) 2022-11-05 21:55:48 +08:00
Zeng1998 3aa44f9f6d fix: delete related resources (#415)
* chore: add icon

* fix: delete all related resources when a memo is deleted

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-05 20:21:13 +08:00
Zeng1998 78b4733fb9 chore: add icon for deleting resource (#414) 2022-11-05 17:41:14 +08:00
winwin2011 37bb3bc546 chore: allow skip version (#411)
* chore: allow skip version

* chore: opacity

* chore: polish

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-04 15:40:48 +00:00
winwin2011 b43bfce254 fix: tag compressed (#412) 2022-11-04 23:39:20 +08:00
boojack 0d6281ef6b chore: update signin page (#410)
* chore: update signin page

* chore: update version `v0.7.1`
2022-11-04 12:28:16 +00:00
boojack 8e2844e0c5 fix: tag regexp (#409)
* chore: enable `no-unused-vars`

* fix: tag regexp
2022-11-04 00:28:29 +00:00
boojack c27e38b11c chore: enable no-unused-vars (#408) 2022-11-03 23:43:56 +00:00
boojack cf75054106 feat: add system setting to allow user signup (#407) 2022-11-03 21:47:36 +08:00
Zhou Yunliang 4ed987229b feat: text filter regex support (#406) 2022-11-03 21:06:17 +08:00
Mahoo Huang a1068b6fe3 fix: abnormal blockquote regexp (#404)
```markdown
面向具体实现编程 ==> 面向抽象接口编程
```.
面向具体实现编程 ==
> 面向抽象接口编程
2022-11-02 20:33:25 +08:00
Zhou Yunliang 91a61e058a feat: view all images of a memo (#393)
* feat: view all images of a memo

* fix: function arguments

* refactor: unified image preview

* refactor: image preview for resource dialog

Co-authored-by: XQ <qiaobingxue1998@163.com>
2022-11-02 12:00:28 +00:00
Zhou Yunliang bebcabc292 chore: update gitignore (#402) 2022-11-02 18:57:45 +08:00
winwin2011 5bdf15aece chore: fold/expand i18n (#400) 2022-11-01 22:44:23 +00:00
winwin2011 d347b7a91e chore: pin button color (#399) 2022-11-02 06:42:20 +08:00
boojack 2c653f170c chore: update corner width (#398) 2022-11-01 15:13:54 +00:00
boojack 75218ef826 chore: update readme (#397) 2022-11-01 15:09:17 +00:00
boojack b7fbbed895 chore: update pagination for getting all memos (#396) 2022-11-01 14:57:33 +00:00
boojack f8c0d73d2d chore: update resource dialog style (#395) 2022-11-01 14:24:54 +00:00
boojack 006cb56d28 fix: heatmap data (#394) 2022-11-01 14:06:02 +00:00
boojack 55dee0df7e fix: session max age (#392) 2022-11-01 13:03:33 +00:00
boojack 2a275b2875 fix: memo list default value (#390) 2022-11-01 09:12:00 +08:00
Zeng1998 4246232fbe feat: image preview enhancement (#385)
* feat: dialog content enhancement

* update

* feat: image carousel

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-01 09:07:14 +08:00
boojack 9d8c9609c3 feat: cache editing memo id (#388)
* feat: cache editing memo id

* chore: update
2022-10-31 21:39:22 +08:00
boojack ef5492074e chore: update memo stats api (#387) 2022-10-31 20:57:07 +08:00
boojack 40d5686031 chore: update unpin memo (#386) 2022-10-31 11:27:56 +00:00
Zeng1998 4276a7a56d feat: remove unused resources (#379)
* feat: dialog content enhancement

* feat: remove unused resources

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-31 11:23:44 +00:00
Zeng1998 d6fa1c7c80 feat: dialog content enhancement (#377) 2022-10-31 11:00:18 +00:00
boojack 6cbc3c78e2 chore: update readme (#375) 2022-10-30 14:42:56 +08:00
boojack 29966451b6 feat: unpin button in the corner (#374)
* feat: unpin button in corner

* chore: update
2022-10-30 05:21:24 +00:00
boojack 64f0662a61 fix: get memo list order (#370) 2022-10-29 12:54:54 +00:00
boojack 0ccfd0c743 fix: resource table migration (#369)
* fix: resource table migration

* chore: update
2022-10-29 11:47:31 +00:00
boojack 0ea1733acc fix: missing column in resource table (#368) 2022-10-29 10:49:58 +00:00
boojack 8fb59bbfb9 chore: release v0.7.0 (#367)
chore: update release version
2022-10-29 10:18:24 +00:00
boojack 7c401040c8 feat: add blockquote regexp (#366) 2022-10-29 10:14:53 +00:00
boojack 43541bde2c feat: add update version banner (#365)
feat: add update version banenr
2022-10-29 09:49:50 +00:00
boojack 1e01c4dc46 chore: update resource service (#364) 2022-10-29 09:24:56 +00:00
Zeng1998 e85d368f87 feat: patch resource filename (#360)
* feat: resource filename rename

* update: resource filename rename

* update: resource filename rename

* update: validation about the filename

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-29 07:40:09 +00:00
boojack 95376f78f6 feat: add metric plugin (#361) 2022-10-29 03:15:39 +00:00
Zeng1998 30daea0c4f fix: typo (#357)
* feat: resource dialog enhancements

* update

* fix: typo

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-29 01:13:11 +00:00
Zeng1998 b891e08928 feat: resource dialog enhancements (#356)
* feat: resource dialog enhancements

* update
2022-10-29 09:11:16 +08:00
boojack 94df09c8c0 chore: update memo list api (#350) 2022-10-27 14:02:42 +00:00
boojack bdf6d4d42a feat: case-insensitive search (#347) 2022-10-27 00:05:45 +00:00
boojack cb2e1ae355 fix: fetch memo with filter (#346) 2022-10-26 23:53:40 +00:00
boojack 9705406b82 feat: remove foreign key and triggers (#345) 2022-10-26 15:00:09 +00:00
Zhou Yunliang 4e00b1b0cd feat: rss support (#343)
* feat: rss support

* chore: go mod tidy

* chore: change route group prefix

* Update server/server.go

Co-authored-by: boojack <stevenlgtm@gmail.com>

* Update server/rss.go

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-26 20:13:02 +08:00
boojack fe5ba6850b chore: update insert content in editor (#336) 2022-10-23 12:28:30 +00:00
Mahoo Huang 5690dab6bb feat: add some hotkeys (#320)
* feat: add some hotkeys

* fix: hotkeys lose the text behind selected

* chore: adjust insertText params passing
2022-10-23 11:33:45 +00:00
Zeng1998 65506a5b30 chore: update the translation (#333) 2022-10-22 09:36:38 +00:00
boojack f09bb2689c chore: release v0.6.1 (#332) 2022-10-21 15:21:37 +00:00
boojack e6f376ae66 fix: user setting name (#331) 2022-10-21 15:06:15 +00:00
boojack 1c2998c4d8 feat: pagination for memo list (#330) 2022-10-21 14:51:41 +00:00
Zeng1998 fc5d5cf231 fix: patch memo with resource list (#328)
fix: 修改memo时添加图片不显示
2022-10-21 22:16:05 +08:00
boojack 2a4fc7dcc3 chore: update memo display time (#327)
* chore: update memo display time

* chore: update
2022-10-21 20:26:00 +08:00
boojack b68d6e2693 feat: update memo sort option setting (#326)
feat: add memo display ts
2022-10-21 11:57:57 +00:00
boojack 0b34b142c8 chore: update marked (#325) 2022-10-21 09:13:19 +08:00
boojack 0b2a9d8511 fix: bold and emphasis regex (#323)
* fix: bold and emphasis regex

* chore: udpate
2022-10-20 21:57:40 +08:00
winwin2011 7c9c5c316b chore: update i18n in settings (#324)
chore: i18n
2022-10-20 21:40:03 +08:00
winwin2011 180ae206c7 feat: inline code within link (#321)
* feat: inline code with link

* fix: decoration style
2022-10-20 21:21:30 +08:00
boojack 69e3ba6bed chore: update demo seeding data (#318)
chore: update seeding data
2022-10-20 17:19:37 +08:00
Zeng1998 bf5b7e747d feat: customize memo list sorting rules (#312)
* chore: update .gitignore

* feat: 添加Memo列表按更新时间排序

* fix go-static-checks

* update

* update

* update Memo.tsx/MemoList.tsx

* handle conflict

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-19 21:00:34 +08:00
f97 24154c95f2 feat: editor tab support (#309)
* feat: editor tab support

* Update web/src/components/MemoEditor.tsx

Co-authored-by: boojack <stevenlgtm@gmail.com>

* chore: if return style

Co-authored-by: boojack <stevenlgtm@gmail.com>
Co-authored-by: hyoban <hi@hyoban.cc>
2022-10-19 18:19:50 +08:00
boojack 0b10d24eb2 chore: update bug report template (#316)
chore: update but report template
2022-10-19 08:22:56 +08:00
boojack 06ec59eca4 chore: add issue template (#314) 2022-10-18 23:13:41 +08:00
boojack 35a8817442 fix: memo list auto scroll to top (#313)
fix: editor initial content
2022-10-18 22:56:43 +08:00
boojack c7378e78d9 chore: update mask animation styles (#306) 2022-10-17 18:43:02 +08:00
Tiger ab182dc22a chore: update zh.json (#304)
Update zh.json
2022-10-16 13:49:58 +08:00
boojack f554e5a357 fix: close button z-index in setting dialog (#303) 2022-10-16 12:37:17 +08:00
Steven 749486ba3c chore: update marked tests 2022-10-15 21:57:56 +08:00
Hyoban 26aae0e637 fix: blank line after table (#298) 2022-10-15 17:51:17 +08:00
Hyoban 95c8d8fc9c docs: update docker-compose tip (#299) 2022-10-15 15:32:42 +08:00
Hyoban 1021023577 docs: docker compose tip (#296)
* docs: docker compose tip

* chore: update

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-15 13:27:06 +08:00
Hyoban 3d0c6004c0 feat: support markdown table (#294)
* feat: support markdown table

* chore: update table style

* test: for markdown table parse
2022-10-15 11:42:04 +08:00
Steven 6c53bd00f6 chore: restore user email 2022-10-15 11:27:12 +08:00
Steven cc759bef56 fix: handle highlight unknown language error 2022-10-15 06:11:17 +08:00
Steven d670adc11f chore: release v0.6.0 2022-10-14 23:25:07 +08:00
Steven 349c383604 chore: reorder memo resource 2022-10-14 23:14:08 +08:00
Steven f407488128 chore: update dev version 2022-10-14 23:03:54 +08:00
boojack 086b10717f feat: upload files by dropping (#292) 2022-10-14 22:59:30 +08:00
boojack eefd0444c8 feat: add highlight for code block (#291)
* feat: add highlight for code block

* chore: update test
2022-10-14 22:29:28 +08:00
Hyoban 65a61ed270 fix: can not click sidebar (#289) 2022-10-14 21:43:43 +08:00
Hyoban 5e7db4631e feat: handle esc keyboard event for editor (#288) 2022-10-14 10:14:35 +08:00
boojack 0d6114e25e feat: update sidebar mask styles (#287) 2022-10-14 07:26:43 +08:00
h2o2o ce5a6fa3ac chore: update sidebar styles in mobile view (#285)
* 添加移动端点击自动关闭侧边栏

* 添加移动端点击自动关闭侧边栏

* 添加移动端点击自动关闭侧边栏

* move closeSidebar function to utils

* move closeSidebar function to utils

* 侧边栏优化

* 移动端侧边栏优化

* 移动端侧边栏优化

* 移动端侧边栏优化
2022-10-13 22:56:42 +08:00
boojack 246851fdbe fix: create memo with visibility (#281) 2022-10-13 09:01:09 +08:00
boojack 21c30ac157 chore: hide user email (#282) 2022-10-13 09:01:02 +08:00
Steven 269d92e637 fix: create triggers after dropping old tables 2022-10-13 08:23:05 +08:00
Steven ffe145d436 chore: revert sidebar updates 2022-10-13 08:05:59 +08:00
zburu 37cd841992 chore: adjust sidebar style (#277) 2022-10-13 07:54:34 +08:00
h2o2o 315ab94c94 添加移动端点击自动关闭侧边栏 (#271) 2022-10-11 08:12:35 +08:00
f97 1aa9963e07 fix: i18n for filter (#275) 2022-10-10 17:46:52 +08:00
Steven 88ade2c0b7 chore: update i18n for filter 2022-10-09 08:54:05 +08:00
winwin2011 4ada7dce77 chore: update i18n for shortcut filter (#270)
* chore: resources i18n

* chore: shortcut-list i18n

* chore: resources i18n

* chore: resources i18n

* chore: resources i18n
2022-10-08 22:27:23 +08:00
steven a323689ee6 chore: update copy button style 2022-10-05 20:55:04 +08:00
f97 2ea612e2fe feat: add copy button to memo (#267)
* feat: copy-content

* Update web/src/less/memo-detail.less

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-10-05 16:39:04 +08:00
boojack ca2557eb7e fix: tag filter (#266) 2022-10-04 21:02:07 +08:00
steven e0f95686f3 chore: update header style 2022-10-04 20:42:33 +08:00
f97 29770e8bfb chore: update vietnamese i18n (#261) 2022-10-04 14:37:44 +08:00
steven b959acc69d fix: update marked test cases 2022-10-04 14:35:07 +08:00
steven 486cf8bdac feat: add escape to renderer 2022-10-04 13:53:36 +08:00
steven ea911387f1 chore: update migration sql file 2022-10-04 12:04:26 +08:00
steven 0b9b89db81 chore: update VACUUM 2022-10-04 10:48:45 +08:00
boojack eaf89aa2f2 feat: update marked parsers (#260)
* chore: remove external match functions

* chore: update parsers
2022-10-04 10:44:16 +08:00
boojack 4bd373ba57 fix: tag selector position (#259) 2022-10-04 08:48:43 +08:00
steven 7b29c65f58 feat: add inline-code syntax parser 2022-10-03 19:51:54 +08:00
steven 2298ac6ff3 chore: add FUNDING.yml 2022-10-03 19:43:56 +08:00
steven 4477f47c25 chore: release version 0.5.0 2022-10-03 18:49:11 +08:00
steven 4a2fe3aaae chore: update user banner style 2022-10-03 18:48:44 +08:00
steven 0f65b8bdd3 fix: re-generate table columns foreign keys 2022-10-03 18:47:31 +08:00
steven 778282ae29 chore: update resource dialog 2022-10-03 09:39:59 +08:00
steven 85dc29bfb9 feat: add linkedMemoAmount to resource 2022-10-03 09:39:49 +08:00
steven 05e46ee4a8 fix: emoji picker style 2022-10-03 09:11:07 +08:00
steven 6a3b052fa2 chore: don't clean data in dev mode 2022-10-03 08:49:20 +08:00
boojack 222c792539 fix: tag regexp (#253) 2022-10-02 23:12:06 +08:00
boojack 51fb8ddb07 feat: simple markdown parser (#252)
* feat: simple markdown parser

* chore: rename test file name

* feat: add plain text link parser

* chore: update style
2022-10-02 22:49:30 +08:00
steven 8e63b8f289 chore: update interval flag type 2022-10-01 23:01:32 +08:00
steven 17c35be426 chore: add --passWithNoTests to jest 2022-10-01 22:59:00 +08:00
steven dfd461518f chore: add jest github action 2022-10-01 22:57:32 +08:00
steven df5818cdc5 chore: add jest 2022-10-01 22:56:20 +08:00
steven 5894104524 chore: update inline image 2022-10-01 20:00:45 +08:00
steven 09732df0f5 chore: regenerate yarn.lock 2022-10-01 19:59:15 +08:00
steven ae8f292306 feat: create memo with resourceIdList 2022-10-01 10:57:14 +08:00
steven 9f8c0ce567 fix: raw data cache 2022-10-01 10:37:02 +08:00
steven c7cf35c7de feat: update memo resource component 2022-10-01 10:02:16 +08:00
steven e5c9d8604d feat: compose memo resource list 2022-10-01 09:49:59 +08:00
steven b2c22977c1 feat: update memo editor with uploading resources 2022-10-01 00:10:31 +08:00
steven c0edb72b3d chore: update react version 2022-10-01 00:07:06 +08:00
steven 33d31b7dca fix: delete memo resource 2022-10-01 00:06:56 +08:00
steven 4c465bef2d feat: add memo resource apis 2022-09-30 22:58:59 +08:00
steven 3bde9543f3 chore: update dev version v0.5.0 2022-09-30 22:58:12 +08:00
steven cff0e86989 feat: add memo_resource model 2022-09-30 20:20:00 +08:00
boojack 65f7aa7914 fix: emoji picker position in fullscreen (#251) 2022-09-30 18:40:46 +08:00
boojack 06f5a5788e fix: mobile editor float style (#247) 2022-09-27 22:58:03 +08:00
boojack 52c8ac2ad3 chore: update emoji picker toggle logic (#244) 2022-09-26 22:33:41 +08:00
Steven 0c80654cc2 chore: update dropdown action button style 2022-09-26 22:06:06 +08:00
Steven a2180f177f chore: remove vite-plugin-pwa 2022-09-26 21:40:37 +08:00
Steven b992d07f3e chore: update memo detail header 2022-09-24 10:05:51 +08:00
Steven dc3052e225 chore: release v0.4.5 2022-09-24 09:41:14 +08:00
Steven 9b2e57cee5 fix: api access checks 2022-09-24 09:34:01 +08:00
Steven 77a3513a6b chore: update memo detail page 2022-09-24 09:31:20 +08:00
Steven 0dd2337663 chore: update router 2022-09-21 21:30:33 +08:00
Steven d316c04837 feat: update dev latest schema 2022-09-21 19:34:54 +08:00
Steven 63468dbaf3 chore: update memo detail access handler 2022-09-21 19:31:02 +08:00
f97 2acd5d4af2 chore: support html form (#236) 2022-09-21 14:12:16 +08:00
Steven 6c1cc1d283 chore: use conditional rendering instead of OnlyWhen 2022-09-20 23:30:25 +08:00
Steven 15cfc9e1f5 chore: add memo detail page 2022-09-20 22:55:24 +08:00
Steven 004713d4cd chore: update dropdown component 2022-09-20 21:11:33 +08:00
f97 7a6eb53e0f feat: float mobile editor (#234)
* feat: float mobile editor

* fix: fix pr comment

* lint: fix golangci-lint
2022-09-20 20:42:14 +08:00
boojack 02c26d5bb4 chore: update i18n for memo visibility (#233) 2022-09-20 08:34:49 +08:00
Steven c60c58ed69 chore: fix memo content click handler 2022-09-19 23:01:14 +08:00
Steven 366afdd1e4 feat: use i18next 2022-09-19 22:27:50 +08:00
Steven 307483e499 feat: use react-router 2022-09-19 21:53:27 +08:00
Steven 4608894e56 fix: add _foreign_keys to sqlite dsn 2022-09-18 22:48:26 +08:00
boojack a1066322c8 chore: add vite-plugin-pwa (#230) 2022-09-18 11:58:03 +08:00
f97 050a2d39fa chore: fix i18n vietnamese typos and update MemoCardDialog (#228) 2022-09-18 09:17:46 +08:00
Steven 13aa61bbc0 chore: update i18n for change password dialog 2022-09-17 13:33:40 +08:00
boojack f4d0e8c948 chore: hide the searchbar of emoji picker (#222)
chore: hide emoji picker searchbar
2022-09-17 09:23:53 +08:00
f97 e7db587a9e feat: add emoji picker in editor (#221)
* feat: add vietnamese

* feat: add emoji picker in editor

* fix failing checks

* move emoji button before upload button

* move script to body index.html

* Update web/index.html

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-09-17 00:42:52 +08:00
Steven 059080f194 chore: fix docker tag 2022-09-16 23:34:57 +08:00
Steven 78c159cd59 chore: fix build tag 2022-09-16 23:15:16 +08:00
Steven 721aa3c907 chore: update v0.4.4 2022-09-16 23:04:40 +08:00
Steven 77178afad5 chore: add no data tip 2022-09-16 22:55:39 +08:00
Steven fd7b8c3293 chore: add copy non-private memo link 2022-09-16 22:49:11 +08:00
Steven 660908e436 chore: add react-router 2022-09-16 22:48:41 +08:00
Steven 8d694f7732 chore: remove language beta badge 2022-09-16 21:47:00 +08:00
f97 bdfa9f7a56 chore: update i18n for UserBanner (#219)
* feat: add vietnamese

* chore: update i18n

* Update web/src/locales/zh.json

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-09-16 21:48:18 +08:00
Steven 19ef97a7ec chore: update dev version 2022-09-16 21:45:56 +08:00
Steven 2c17ea703d chore: fix manifest.json image 2022-09-16 21:33:31 +08:00
Steven 1591fdf61c chore: update i18n structures 2022-09-16 21:25:39 +08:00
f97 811f3340e9 feat: add vietnamese (#218) 2022-09-16 21:09:59 +08:00
ChasLui 7e8d1128f8 chore: update i18n (#215) 2022-09-15 06:53:51 +08:00
Steven 54db6eda04 chore: update common locales 2022-09-14 19:35:45 +08:00
boojack c5b26e3310 chore: fix copy to clipboard (#214) 2022-09-14 19:24:13 +08:00
ChasLui 7079faf2b9 chore: update i18n (#212)
* feat: 增加部分 i18n

* feat: 增加部分 i18n

* feat: 增加部分 i18n
2022-09-14 19:14:45 +08:00
Steven 707d1a96eb chore: move version pkg to server/version 2022-09-12 17:25:34 +08:00
boojack 76801dfa4f chore: vacuum db file after deleting resource (#210) 2022-09-10 23:43:19 +08:00
Steven 6e4577f721 feat: add MemoContent component 2022-09-10 21:22:26 +08:00
winwin2011 7b0987610c feat: i18n for dialogs (#203)
* feat: i18n in dialog

* fix: use common translation
2022-09-10 09:30:10 +08:00
boojack 50fa560d4a chore: update username validate config (#209) 2022-09-09 22:30:24 +08:00
Steven b8a7df21f2 chore: release v0.4.3 2022-09-09 20:00:04 +08:00
boojack d1a4348048 chore: support double-click to edit memo (#207) 2022-09-09 19:42:04 +08:00
Steven 020b211660 chore: update explore page style 2022-09-09 19:41:12 +08:00
Steven 4657e58b52 chore: update loading section 2022-09-09 08:22:49 +08:00
Steven 22971c3a93 chore: support upload more file type 2022-09-09 08:10:39 +08:00
Steven 5fa9aa3c22 chore: add memo resources component 2022-09-09 07:53:02 +08:00
Steven fbce43870f chore: update resource base url 2022-09-09 07:40:21 +08:00
Steven b1e6956441 chore: add cache for resource 2022-09-09 00:50:58 +08:00
Steven ad462cec29 chore: update explore style 2022-09-09 00:37:31 +08:00
Steven a0a42285d0 chore: update docs 2022-09-09 00:32:01 +08:00
boojack b0b2776d03 chore: update button entries (#206) 2022-09-09 00:24:41 +08:00
boojack e9ac6affef feat: add explore page (#205) 2022-09-09 00:06:05 +08:00
winwin2011 5eea1339c9 fix: upload resouces double time (#204) 2022-09-08 00:13:21 +08:00
Steven f303dc21a0 chore: add get all memo api 2022-09-05 21:14:17 +08:00
Steven d68891d91d chore: fix tag regex 2022-09-05 20:15:34 +08:00
winwin2011 987bb80770 fix: heatmap popover blink (#195) 2022-09-04 22:34:24 +08:00
Steven b884327a53 chore: update eslint rules 2022-09-04 06:48:19 +08:00
Steven 4743818fe7 chore: update not found handler in deleting 2022-09-03 18:54:22 +08:00
Steven 43575e6f54 chore: update readme 2022-09-03 18:46:16 +08:00
Steven 89f9dc5640 chore: update resources 2022-09-03 18:41:54 +08:00
Steven f7aca99d52 fix: upload image in iOS safari 2022-09-03 00:10:50 +08:00
Steven 12a48ae2db chore: release v0.4.2 2022-09-02 20:20:00 +08:00
Steven bcb684d1cc chore: fix daily memo style 2022-09-02 20:13:56 +08:00
Steven d3b26f7126 chore: update about dialog 2022-09-02 20:12:13 +08:00
boojack 422e190c96 chore: support uploads multi files (#191) 2022-09-02 20:05:40 +08:00
Steven 3e13fa1ce6 chore: update marked helper 2022-09-02 09:07:32 +08:00
boojack dc9f531447 fix: find latest migration history (#190)
* fix: auth action button

* fix: find latest migration history
2022-09-02 00:01:08 +08:00
Steven e330159f55 chore: update resources file format 2022-09-01 07:36:14 +08:00
boojack b88846fff5 chore: update readme 2022-08-31 21:55:18 +08:00
Steven 93b6a910ae chore: update logo resources 2022-08-31 21:51:49 +08:00
boojack 8d161b4526 fix: create shortcut input (#186)
* fix: create shortcut input

* chore: add yarn build
2022-08-31 20:21:56 +08:00
boojack b6acf62aab chore: add eslint check in action (#185)
* chore: add eslint check in action

* chore: update cache path
2022-08-31 19:27:17 +08:00
Steven 2a11aed881 chore: update dialog event listener 2022-08-30 07:41:28 +08:00
zburu e7b287902b chore: update mobile style (#181) 2022-08-29 20:03:07 +08:00
TheNexter c012ce0481 Simple docker-compose.yml (#179)
* Create docker-compose.yml

* Add docker compose in readme.md

* Good service name in docker-compose.yml

* Update docker-compose.yml

Change 2.1 to 3.0 version + path

* Update docker-compose.yml

Change 2.1 to 3.0 version + path

* Update readme

Change docker-compose to 2.0 + path

* Update README.md

* Update README.md

Co-authored-by: boojack <imrealleonardo@gmail.com>
2022-08-29 19:59:24 +08:00
steven c97399a8ea fix: update isFetching default value 2022-08-28 00:30:26 +08:00
steven 75d622f4a2 chore: update version v0.4.1 2022-08-27 08:58:13 +08:00
steven 5bdf7654fc chore: update detail styles 2022-08-27 08:57:29 +08:00
steven 62657f7f4e chore: update build folder 2022-08-27 08:57:05 +08:00
Steven 64332c3e6a chore: update tag regexp 2022-08-25 23:58:03 +08:00
Steven 57f51d1c58 feat: allow updating memo createdTs 2022-08-25 22:02:32 +08:00
Steven 9f3f730723 chore: update selector style 2022-08-25 21:05:31 +08:00
boojack e9d303326f feat: set editor font style (#174)
feat: editor font style
2022-08-25 20:44:32 +08:00
Steven 20d7112a05 chore: update config files 2022-08-25 20:44:17 +08:00
Steven 922cc21abc chore: update seed data 2022-08-25 19:53:30 +08:00
boojack cdbd934c4e fix: tag regexp (#173) 2022-08-25 19:24:21 +08:00
Steven ca1170583e chore: update i18n 2022-08-24 22:27:19 +08:00
Steven f5629c8227 fix: overflow style 2022-08-24 22:14:46 +08:00
Steven 2f58032ad8 fix: text overflow style 2022-08-24 22:11:56 +08:00
boojack 466bfe4b49 chore: update golangci-lint config (#168)
chore: update ci lint
2022-08-24 22:03:07 +08:00
Steven 7d0407013e chore: make golangci-lint happy 2022-08-24 21:53:12 +08:00
Steven 0e4e2e4bc5 chore: add golangci-lint action 2022-08-24 21:12:08 +08:00
Steven a8a3cf31b4 chore: make golangci-lint happy 2022-08-24 20:40:56 +08:00
boojack f784516470 chore: update word-break style (#164) 2022-08-23 23:23:20 +08:00
Steven 2aed7c70aa chore: update cancel editing button 2022-08-23 21:33:53 +08:00
Lim Ding Wen b1062f5387 fix: Fix Typo (Editting -> Editing) (#161) 2022-08-23 07:50:57 +08:00
Steven 29c835d27a chore: update readme 2022-08-22 20:52:54 +08:00
Steven 0698c9c853 chore: set default flags in dockerfile 2022-08-22 20:52:30 +08:00
boojack e54ff5ec9e fix: unescape filename (#158) 2022-08-22 20:26:47 +08:00
Steven f0a23f4620 fix: generate uuid when creating new user 2022-08-21 19:10:02 +08:00
Steven c60bb12424 chore: update user setting validator 2022-08-20 21:51:28 +08:00
Steven 3b1bb4a95d chore: disable setting memo visibility when creating 2022-08-20 21:22:36 +08:00
Steven 05a5c59a7e chore: update user create validator 2022-08-20 21:03:15 +08:00
Steven 734d5f3aed chore: update create tag tip style 2022-08-20 11:48:56 +08:00
Steven 2cf07753e7 chore: update version 0.4.0 2022-08-20 11:37:19 +08:00
Steven 2935057ca7 chore: update i18n 2022-08-20 11:36:33 +08:00
Steven 68b30063a9 chore: update prod schema 2022-08-20 11:36:24 +08:00
Steven f06a3d171b chore: update error message handler 2022-08-20 07:34:39 +08:00
Steven a98e64cf0a chore: update i18n dialog 2022-08-19 22:12:24 +08:00
Steven dd04bc9e1d chore: add beta badge 2022-08-19 22:08:47 +08:00
Steven 2f33eceada chore: set default memo visibility 2022-08-19 21:59:50 +08:00
Steven d5b88775d9 chore: update user setting keys 2022-08-19 21:56:22 +08:00
Steven a7a01df79a chore: update i18n 2022-08-19 21:30:31 +08:00
Steven e3fac742c5 chore: update logger 2022-08-19 00:45:02 +08:00
Steven ce390f3f79 chore: add memo visibility with user setting 2022-08-19 00:00:47 +08:00
Steven 43a8b7d0e1 chore: fix lint 2022-08-14 18:44:20 +08:00
Steven b596d04939 chore: update i18n for auth page 2022-08-14 18:43:46 +08:00
Steven 84a3548232 chore: update scripts 2022-08-14 17:55:36 +08:00
Steven 63388a7d97 chore: update .gitignore 2022-08-14 16:44:00 +08:00
Steven 35f980d2b8 chore: go mod tidy 2022-08-14 16:35:09 +08:00
boojack 90b881502d feat: add user_setting model (#145)
* feat: add `user_setting` model

* chore: add global store

* chore: update settings in web

* chore: update `i18n` example
2022-08-13 14:35:33 +08:00
XQ dfac877957 feat: add manifest.json for pwa (#144)
* chore: update `i18nStore`

* feat: add pwa `manifest.json`
2022-08-12 21:30:59 +08:00
leopold 87f5ac8b71 chore: add docker-compose.yaml (#142)
Adding docker-compose.yaml
2022-08-11 20:15:46 +08:00
XQ 3c1a416afc chore: update i18nStore (#141) 2022-08-09 21:57:56 +08:00
XQ 972a49d6aa chore: code clean (#140) 2022-08-09 21:36:48 +08:00
XQ cea16fac88 chore: remove useAppContext in dialog props 2022-08-08 23:33:53 +08:00
boojack 7e994c8f11 chore: remove test action 2022-08-08 23:32:56 +08:00
boojack 646a41e931 chore: add i18n based with useContext 2022-08-07 22:48:22 +08:00
boojack 735938395b chore: use transaction for migration history 2022-08-07 10:17:56 +08:00
boojack d8e10ba399 chore: use tx for stores 2022-08-07 10:17:12 +08:00
boojack 8c28721839 chore: use tx for user store 2022-08-07 09:23:46 +08:00
boojack da333b0b1e chore: add store cache service 2022-08-07 08:09:43 +08:00
boojack 44d07ac401 chore: hide resource blob field 2022-08-07 07:07:04 +08:00
boojack d08ba56c28 chore: release v0.3.1 (#138) 2022-08-07 02:20:39 +08:00
boojack c991a48df6 chore: add upload resource button 2022-08-07 01:56:10 +08:00
boojack fd44255668 chore: use dropdown in member section 2022-08-07 01:35:20 +08:00
boojack 84564891be feat: add view resource dialog 2022-08-07 01:30:48 +08:00
boojack 8c8bb9e59f chore: update search bar styles 2022-07-31 09:10:30 +08:00
boojack 99df4acfe9 chore: use cors middleware 2022-07-30 14:52:37 +08:00
boojack 47ffd99c3b chore: fix typo 2022-07-30 07:47:18 +08:00
boojack c703f281d9 chore: update feather icon 2022-07-30 00:29:20 +08:00
boojack e179c65e52 chore: fix typo 2022-07-29 21:41:56 +08:00
boojack 29da70be56 chore: update readme 2022-07-29 21:39:52 +08:00
boojack a9dce26099 chore: remove build dev image 2022-07-29 21:26:08 +08:00
boojack 2c27f5d425 chore: release v0.3.0 (#136) 2022-07-29 21:09:48 +08:00
boojack df7b4d54c6 chore: show inline image in daily review dialog (#135) 2022-07-29 20:11:14 +08:00
boojack 9994b1fabc chore: update member setting styles 2022-07-29 19:52:16 +08:00
boojack 2d093d5be0 chore: update daily review dialog style 2022-07-28 23:38:09 +08:00
boojack 12b373701b chore: fix shortcut list buttons style 2022-07-28 21:08:18 +08:00
boojack 2b8078a19b chore: add CommonDialog 2022-07-28 20:19:14 +08:00
boojack 5617118fa8 fix: acl middleware 2022-07-28 20:09:25 +08:00
boojack fa93d0fd6e chore: update visibility selector style 2022-07-27 20:02:00 +08:00
boojack dc436490f8 chore: update build.sh 2022-07-27 19:47:13 +08:00
boojack d83f204d8c chore: update acl middleware 2022-07-27 19:45:37 +08:00
boojack 873973a088 chore: update favicon 2022-07-26 22:40:29 +08:00
boojack d371cfd78d chore: update member list action buttons 2022-07-26 22:36:24 +08:00
boojack 7b1bad5b29 feat: update delete user api 2022-07-26 22:32:26 +08:00
boojack 0c2adfa1d2 feat: add delete user api 2022-07-26 21:41:20 +08:00
boojack 07d9649b22 chore: add visibility selector 2022-07-26 21:24:52 +08:00
boojack b7339e00ba feat: update finding memo with visibility 2022-07-26 21:12:20 +08:00
boojack 58e68f8f80 chore: update signin button in visitor mode 2022-07-25 21:50:25 +08:00
boojack cfa4151cff chore: update migration folder 2022-07-25 21:17:46 +08:00
boojack 3d33b5d564 chore: update memo visibility field (#132)
chore: update `memo` visibility field in schema
2022-07-24 21:01:56 +08:00
boojack b516a8561f chore: update readme 2022-07-24 13:37:07 +08:00
boojack 38383a426f chore: update error message (#129) 2022-07-24 00:29:19 +08:00
boojack 7e34de23f1 chore: update live demo link 2022-07-23 19:47:14 +08:00
boojack f02ec375a6 chore: release v0.2.2 (#127) 2022-07-22 23:51:58 +08:00
291 changed files with 14548 additions and 5363 deletions
+1
View File
@@ -0,0 +1 @@
ko_fi: stevenlgtm
+31
View File
@@ -0,0 +1,31 @@
name: Bug Report
description: Create a report to help us improve
labels: [bug]
body:
- type: markdown
attributes:
value: |
If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
- type: textarea
attributes:
label: Describe the bug
description: |
Briefly describe the problem you are having in a few paragraphs.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: |
Provide the steps to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '....'
3. See error
validations:
required: true
- type: textarea
attributes:
label: Screenshots or additional context
description: |
Add screenshots or any other context about the problem.
@@ -0,0 +1,28 @@
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest an idea for Memos!
- type: textarea
attributes:
label: Is your feature request related to a problem?
description: |
A clear and concise description of what the problem is.
placeholder: |
I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: |
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request.
@@ -1,37 +0,0 @@
name: build-and-push-dev-image
on:
push:
branches:
- "main"
jobs:
build-and-push-dev-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: neosmemo/memos:dev
@@ -4,7 +4,7 @@ on:
push:
branches:
# Run on pushing branches like `release/1.0.0`
- "release/v*.*.*"
- "release/*.*.*"
jobs:
build-and-push-release-image:
@@ -17,9 +17,9 @@ jobs:
- name: Extract build args
# Extract version from branch name
# Example: branch name `release/v1.0.0` sets up env.VERSION=1.0.0
# Example: branch name `release/1.0.0` sets up env.VERSION=1.0.0
run: |
echo "VERSION=${GITHUB_REF_NAME#release/v}" >> $GITHUB_ENV
echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -2,11 +2,10 @@ name: build-and-push-test-image
on:
push:
branches:
- "test/*"
branches: [main]
jobs:
build-and-push-dev-image:
build-and-push-test-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -32,6 +31,6 @@ jobs:
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
tags: neosmemo/memos:test
+28 -30
View File
@@ -13,12 +13,10 @@ name: "CodeQL"
on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '27 12 * * 0'
branches: [main]
jobs:
analyze:
@@ -32,39 +30,39 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
language: ["go", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+88
View File
@@ -0,0 +1,88 @@
name: Test
on:
push:
branches:
- main
- "release/v*.*.*"
pull_request:
branches: [main]
jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run eslint check
run: yarn lint
working-directory: web
jest-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run jest
run: yarn test
working-directory: web
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run frontend build
run: yarn build
working-directory: web
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.19
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Run all tests
run: go test -v ./... | tee test.log; exit ${PIPESTATUS[0]}
- name: Pretty print tests running time
run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}'
+13 -2
View File
@@ -1,10 +1,21 @@
# Air (hot reload) generated
.air
# temp folder
tmp
# Frontend asset
web/dist
# build folder
memos-build
build
.DS_Store
.DS_Store
# Jetbrains
.idea
# vscode
.vscode
bin/air
+64
View File
@@ -0,0 +1,64 @@
linters:
enable:
- goimports
- revive
- govet
- staticcheck
- misspell
- gocritic
- sqlclosecheck
- rowserrcheck
- nilerr
- godot
issues:
exclude:
- Rollback
- fmt.Printf
- fmt.Print
linters-settings:
revive:
enable-all-rules: true
rules:
- name: file-header
disabled: true
- name: line-length-limit
disabled: true
- name: function-length
disabled: true
- name: max-public-structs
disabled: true
- name: function-result-limit
disabled: true
- name: banned-characters
disabled: true
- name: argument-limit
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: confusing-results
disabled: true
- name: add-constant
disabled: true
- name: flag-parameter
disabled: true
- name: nested-structs
disabled: true
- name: import-shadowing
disabled: true
- name: early-return
disabled: true
gocritic:
disabled-checks:
- ifElseChain
govet:
settings:
printf:
funcs:
- common.Errorf
forbidigo:
forbid:
- 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?'
+4
View File
@@ -0,0 +1,4 @@
{
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint"
}
+5 -7
View File
@@ -1,5 +1,5 @@
# Build frontend dist.
FROM node:16.15.0-alpine AS frontend
FROM node:18.12.1-alpine3.16 AS frontend
WORKDIR /frontend-build
COPY ./web/ .
@@ -8,7 +8,7 @@ RUN yarn
RUN yarn build
# Build backend exec file.
FROM golang:1.18.3-alpine3.16 AS backend
FROM golang:1.19.3-alpine3.16 AS backend
WORKDIR /backend-build
RUN apk update
@@ -17,12 +17,10 @@ RUN apk --no-cache add gcc musl-dev
COPY . .
COPY --from=frontend /frontend-build/dist ./server/dist
RUN go build \
-o memos \
./bin/server/main.go
RUN go build -o memos ./bin/server/main.go
# Make workspace with above generated files.
FROM alpine:3.16.0 AS monolithic
FROM alpine:3.16 AS monolithic
WORKDIR /usr/local/memos
COPY --from=backend /backend-build/memos /usr/local/memos/
@@ -30,4 +28,4 @@ COPY --from=backend /backend-build/memos /usr/local/memos/
# Directory to store the data, which can be referenced as the mounting point.
RUN mkdir -p /var/opt/memos
ENTRYPOINT ["./memos"]
ENTRYPOINT ["./memos", "--mode", "prod", "--port", "5230"]
+39 -55
View File
@@ -1,89 +1,73 @@
<h1 align="center">✍️ Memos</h1>
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
<p align="center">An open source, self-hosted knowledge base that works with a SQLite db file.</p>
<p align="center">An open-source, self-hosted memo hub with knowledge management and socialization.</p>
<p align="center">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" />
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
</p>
<p align="center">
<a href="https://memos.onrender.com/">Live Demo</a> •
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
<a href="https://demo.usememos.com/">Live Demo</a> •
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <b><a href="https://discord.gg/tfPJa4UmAv">Discord 🏂</a></b>
</p>
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
## Features
## Features
- 🦄 Fully open source;
- 📜 Writing in plain textarea without any burden,
- and support some useful markdown syntax 💪.
- 🌄 Share the memo in a pretty image or personal page like Twitter;
- 🚀 Fast self-hosting with `Docker`;
- 🤠 Pleasant UI and UX;
- 🦄 Open source and free forever;
- 🚀 Support for self-hosting with `Docker` in seconds;
- 📜 Plain textarea first and support some useful markdown syntax;
- 👥 Set memo private or public to others;
- 🧑‍💻 RESTful API for self-service.
## ⚓️ Deploy with Docker
## Deploy with Docker in seconds
### Docker Run
```docker
docker run \
--name memos \
--publish 5230:5230 \
--volume ~/.memos/:/var/opt/memos \
neosmemo/memos:latest \
--mode prod \
--port 5230
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
```
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it. Memos will be running at [http://localhost:5230](http://localhost:5230).
⚠️ Please DO NOT use `dev` tag of docker image if you have no experience.
### Docker Compose
## 🏗 Development
Example Compose YAML file: [`docker-compose.yaml`](./docker-compose.yaml).
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
If you want to upgrade the version of memos, use the following command.
1. It has no external dependency.
2. It requires zero config.
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
```sh
docker-compose down && docker image rm neosmemo/memos:latest && docker-compose up -d
```
### Tech Stack
## Contribute
<img alt="tech stack" src="https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png" width="360" />
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
### Prerequisites
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
- [Go](https://golang.org/doc/install) (1.16 or later)
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
- [yarn](https://yarnpkg.com/getting-started/install)
## Products made by Community
### Steps
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - Wechat miniprogram
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
- [eallion/memos.top](https://github.com/eallion/memos.top) - A static page rendered with the Memos API
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - A logseq Plugin
1. pull source code
### Join the community to build memos together!
```bash
git clone https://github.com/usememos/memos
```
<a href="https://github.com/usememos/memos/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usememos/memos" />
</a>
2. start backend using air(with live reload)
## License
```bash
air -c scripts/.air.toml
```
This project is open source and available under the [MIT License](https://github.com/usememos/memos/blob/main/LICENSE).
3. start frontend dev server
```bash
cd web && yarn && yarn dev
```
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
## 🌟 Star history
## Star history
[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date)
---
Just enjoy it.
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
## Reporting a bug
Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues).
For more information, please contact [stevenlgtm@gmail.com](stevenlgtm@gmail.com).
+3 -4
View File
@@ -1,13 +1,12 @@
package api
type Signin struct {
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
}
type Signup struct {
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Role Role `json:"role"`
}
+22
View File
@@ -0,0 +1,22 @@
package api
// CacheNamespace is the type of a cache.
type CacheNamespace string
const (
// UserCache is the cache type of users.
UserCache CacheNamespace = "u"
// MemoCache is the cache type of memos.
MemoCache CacheNamespace = "m"
// ShortcutCache is the cache type of shortcuts.
ShortcutCache CacheNamespace = "s"
// ResourceCache is the cache type of resources.
ResourceCache CacheNamespace = "r"
)
// CacheService is the service for caches.
type CacheService interface {
FindCache(namespace CacheNamespace, id int, entry interface{}) (bool, error)
UpsertCache(namespace CacheNamespace, id int, entry interface{}) error
DeleteCache(namespace CacheNamespace, id int)
}
+26 -11
View File
@@ -6,15 +6,19 @@ type Visibility string
const (
// Public is the PUBLIC visibility.
Public Visibility = "PUBLIC"
// Privite is the PRIVATE visibility.
Privite Visibility = "PRIVATE"
// Protected is the PROTECTED visibility.
Protected Visibility = "PROTECTED"
// Private is the PRIVATE visibility.
Private Visibility = "PRIVATE"
)
func (e Visibility) String() string {
switch e {
case Public:
return "PUBLIC"
case Privite:
case Protected:
return "PROTECTED"
case Private:
return "PRIVATE"
}
return "PRIVATE"
@@ -33,28 +37,39 @@ type Memo struct {
Content string `json:"content"`
Visibility Visibility `json:"visibility"`
Pinned bool `json:"pinned"`
DisplayTs int64 `json:"displayTs"`
// Related fields
Creator *User `json:"creator"`
ResourceList []*Resource `json:"resourceList"`
}
type MemoCreate struct {
// Standard fields
CreatorID int
// Used to import memos with a clearly created ts.
CreatedTs *int64 `json:"createdTs"`
// Domain specific fields
Content string `json:"content"`
Visibility *Visibility `json:"visibility"`
Visibility Visibility `json:"visibility"`
Content string `json:"content"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
}
type MemoPatch struct {
ID int
// Standard fields
CreatedTs *int64 `json:"createdTs"`
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Content *string `json:"content"`
Visibility *Visibility `json:"visibility"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
}
type MemoFind struct {
@@ -65,9 +80,9 @@ type MemoFind struct {
CreatorID *int `json:"creatorId"`
// Domain specific fields
Pinned *bool
ContentSearch *string
Visibility *Visibility
Pinned *bool
ContentSearch *string
VisibilityList []Visibility
// Pagination
Limit int
@@ -75,5 +90,5 @@ type MemoFind struct {
}
type MemoDelete struct {
ID int `json:"id"`
ID int
}
+5
View File
@@ -19,3 +19,8 @@ type MemoOrganizerUpsert struct {
UserID int
Pinned bool `json:"pinned"`
}
type MemoOrganizerDelete struct {
MemoID *int
UserID *int
}
+24
View File
@@ -0,0 +1,24 @@
package api
type MemoResource struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
type MemoResourceUpsert struct {
MemoID int
ResourceID int
UpdatedTs *int64
}
type MemoResourceFind struct {
MemoID *int
ResourceID *int
}
type MemoResourceDelete struct {
MemoID *int
ResourceID *int
}
+15 -1
View File
@@ -10,9 +10,12 @@ type Resource struct {
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"blob"`
Blob []byte `json:"-"`
Type string `json:"type"`
Size int64 `json:"size"`
// Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"`
}
type ResourceCreate struct {
@@ -32,6 +35,17 @@ type ResourceFind struct {
// Standard fields
CreatorID *int `json:"creatorId"`
// Domain specific fields
Filename *string `json:"filename"`
MemoID *int
}
type ResourcePatch struct {
ID int
// Standard fields
UpdatedTs *int64
// Domain specific fields
Filename *string `json:"filename"`
}
+5 -1
View File
@@ -27,6 +27,7 @@ type ShortcutPatch struct {
ID int
// Standard fields
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
@@ -45,5 +46,8 @@ type ShortcutFind struct {
}
type ShortcutDelete struct {
ID int
ID *int
// Standard fields
CreatorID *int
}
+11 -2
View File
@@ -3,6 +3,15 @@ package api
import "github.com/usememos/memos/server/profile"
type SystemStatus struct {
Host *User `json:"host"`
Profile *profile.Profile `json:"profile"`
Host *User `json:"host"`
Profile profile.Profile `json:"profile"`
DBSize int64 `json:"dbSize"`
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Additional style.
AdditionalStyle string `json:"additionalStyle"`
// Additional script.
AdditionalScript string `json:"additionalScript"`
}
+87
View File
@@ -0,0 +1,87 @@
package api
import (
"encoding/json"
"fmt"
)
type SystemSettingName string
const (
// SystemSettingAllowSignUpName is the key type of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
// SystemSettingAdditionalStyleName is the key type of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
)
func (key SystemSettingName) String() string {
switch key {
case SystemSettingAllowSignUpName:
return "allowSignUp"
case SystemSettingAdditionalStyleName:
return "additionalStyle"
case SystemSettingAdditionalScriptName:
return "additionalScript"
}
return ""
}
var (
SystemSettingAllowSignUpValue = []bool{true, false}
)
type SystemSetting struct {
Name SystemSettingName
// Value is a JSON string with basic value
Value string
Description string
}
type SystemSettingUpsert struct {
Name SystemSettingName `json:"name"`
Value string `json:"value"`
Description string `json:"description"`
}
func (upsert SystemSettingUpsert) Validate() error {
if upsert.Name == SystemSettingAllowSignUpName {
value := false
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting allow signup value")
}
invalid := true
for _, v := range SystemSettingAllowSignUpValue {
if value == v {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid system setting allow signup value")
}
} else if upsert.Name == SystemSettingAdditionalStyleName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional style value")
}
} else if upsert.Name == SystemSettingAdditionalScriptName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional script value")
}
} else {
return fmt.Errorf("invalid system setting name")
}
return nil
}
type SystemSettingFind struct {
Name *SystemSettingName `json:"name"`
}
+41 -12
View File
@@ -1,11 +1,17 @@
package api
import (
"fmt"
)
// Role is the type of a role.
type Role string
const (
// Host is the HOST role.
Host Role = "HOST"
// Admin is the ADMIN role.
Admin Role = "ADMIN"
// NormalUser is the USER role.
NormalUser Role = "USER"
)
@@ -14,6 +20,8 @@ func (e Role) String() string {
switch e {
case Host:
return "HOST"
case Admin:
return "ADMIN"
case NormalUser:
return "USER"
}
@@ -29,32 +37,48 @@ type User struct {
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
Username string `json:"username"`
Role Role `json:"role"`
Email string `json:"email"`
Nickname string `json:"nickname"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
UserSettingList []*UserSetting `json:"userSettingList"`
}
type UserCreate struct {
// Domain specific fields
Email string `json:"email"`
Username string `json:"username"`
Role Role `json:"role"`
Name string `json:"name"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Password string `json:"password"`
PasswordHash string
OpenID string
}
func (create UserCreate) Validate() error {
if len(create.Username) < 4 {
return fmt.Errorf("username is too short, minimum length is 4")
}
if len(create.Password) < 4 {
return fmt.Errorf("password is too short, minimum length is 4")
}
return nil
}
type UserPatch struct {
ID int
// Standard fields
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Username *string `json:"username"`
Email *string `json:"email"`
Name *string `json:"name"`
Nickname *string `json:"nickname"`
Password *string `json:"password"`
ResetOpenID *bool `json:"resetOpenId"`
PasswordHash *string
@@ -68,8 +92,13 @@ type UserFind struct {
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Email *string `json:"email"`
Role *Role
Name *string `json:"name"`
OpenID *string
Username *string `json:"username"`
Role *Role
Email *string `json:"email"`
Nickname *string `json:"nickname"`
OpenID *string
}
type UserDelete struct {
ID int
}
+140
View File
@@ -0,0 +1,140 @@
package api
import (
"encoding/json"
"fmt"
)
type UserSettingKey string
const (
// UserSettingLocaleKey is the key type for user locale.
UserSettingLocaleKey UserSettingKey = "locale"
// UserSettingAppearanceKey is the key type for user appearance.
UserSettingAppearanceKey UserSettingKey = "appearance"
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
UserSettingMemoDisplayTsOptionKey UserSettingKey = "memoDisplayTsOption"
)
// String returns the string format of UserSettingKey type.
func (key UserSettingKey) String() string {
switch key {
case UserSettingLocaleKey:
return "locale"
case UserSettingAppearanceKey:
return "appearance"
case UserSettingMemoVisibilityKey:
return "memoVisibility"
case UserSettingMemoDisplayTsOptionKey:
return "memoDisplayTsOption"
}
return ""
}
var (
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr"}
UserSettingAppearanceValue = []string{"light", "dark"}
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
)
type UserSetting struct {
UserID int
Key UserSettingKey `json:"key"`
// Value is a JSON string with basic value
Value string `json:"value"`
}
type UserSettingUpsert struct {
UserID int
Key UserSettingKey `json:"key"`
Value string `json:"value"`
}
func (upsert UserSettingUpsert) Validate() error {
if upsert.Key == UserSettingLocaleKey {
localeValue := "en"
err := json.Unmarshal([]byte(upsert.Value), &localeValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting locale value")
}
invalid := true
for _, value := range UserSettingLocaleValue {
if localeValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting locale value")
}
} else if upsert.Key == UserSettingAppearanceKey {
appearanceValue := "light"
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting appearance value")
}
invalid := true
for _, value := range UserSettingAppearanceValue {
if appearanceValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting appearance value")
}
} else if upsert.Key == UserSettingMemoVisibilityKey {
memoVisibilityValue := Private
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
}
invalid := true
for _, value := range UserSettingMemoVisibilityValue {
if memoVisibilityValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo visibility value")
}
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
memoDisplayTsOption := "created_ts"
err := json.Unmarshal([]byte(upsert.Value), &memoDisplayTsOption)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo display ts option")
}
invalid := true
for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
if memoDisplayTsOption == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo display ts option value")
}
} else {
return fmt.Errorf("invalid user setting key")
}
return nil
}
type UserSettingFind struct {
UserID int
Key *UserSettingKey `json:"key"`
}
type UserSettingDelete struct {
UserID int
}
-63
View File
@@ -1,63 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/usememos/memos/server"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
DB "github.com/usememos/memos/store/db"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
type Main struct {
profile *profile.Profile
}
func (m *Main) Run() error {
db := DB.NewDB(m.profile)
if err := db.Open(); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
s := server.NewServer(m.profile)
storeInstance := store.New(db.Db, m.profile)
s.Store = storeInstance
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", m.profile.Version, m.profile.Port)
return s.Run()
}
func Execute() {
profile := profile.GetProfile()
m := Main{
profile: profile,
}
println("---")
println("profile")
println("mode:", profile.Mode)
println("port:", profile.Port)
println("dsn:", profile.DSN)
println("version:", profile.Version)
println("---")
if err := m.Run(); err != nil {
fmt.Printf("error: %+v\n", err)
os.Exit(1)
}
}
+77 -2
View File
@@ -1,7 +1,82 @@
package main
import "github.com/usememos/memos/bin/server/cmd"
import (
"os"
_ "github.com/mattn/go-sqlite3"
"context"
"fmt"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/server"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
DB "github.com/usememos/memos/store/db"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
func run(profile *profile.Profile) error {
ctx := context.Background()
db := DB.NewDB(profile)
if err := db.Open(ctx); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
serverInstance := server.NewServer(profile)
storeInstance := store.New(db.Db, profile)
serverInstance.Store = storeInstance
metricCollector := server.NewMetricCollector(profile, storeInstance)
// Disable metrics collector.
metricCollector.Enabled = false
serverInstance.Collector = &metricCollector
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
metricCollector.Collect(ctx, &metric.Metric{
Name: "service started",
})
return serverInstance.Run()
}
func execute() error {
profile, err := profile.GetProfile()
if err != nil {
return err
}
println("---")
println("profile")
println("mode:", profile.Mode)
println("port:", profile.Port)
println("dsn:", profile.DSN)
println("version:", profile.Version)
println("---")
if err := run(profile); err != nil {
fmt.Printf("error: %+v\n", err)
return err
}
return nil
}
func main() {
cmd.Execute()
if err := execute(); err != nil {
os.Exit(1)
}
}
+1 -6
View File
@@ -9,7 +9,7 @@ type Code int
// Application error codes.
const (
// 0 ~ 99 general error
// 0 ~ 99 general error.
Ok Code = 0
Internal Code = 1
NotAuthorized Code = 2
@@ -17,11 +17,6 @@ const (
NotFound Code = 4
Conflict Code = 5
NotImplemented Code = 6
// 101 ~ 199 db error
DbConnectionFailure Code = 101
DbStatementSyntaxError Code = 102
DbExecutionError Code = 103
)
// Error represents an application-specific error. Application errors can be
+16
View File
@@ -1,6 +1,7 @@
package common
import (
"net/mail"
"strings"
"github.com/google/uuid"
@@ -16,6 +17,21 @@ func HasPrefixes(src string, prefixes ...string) bool {
return false
}
// ValidateEmail validates the email.
func ValidateEmail(email string) bool {
if _, err := mail.ParseAddress(email); err != nil {
return false
}
return true
}
func GenUUID() string {
return uuid.New().String()
}
func Min(x, y int) int {
if x < y {
return x
}
return y
}
+31
View File
@@ -0,0 +1,31 @@
package common
import (
"testing"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
email string
want bool
}{
{
email: "t@gmail.com",
want: true,
},
{
email: "@qq.com",
want: false,
},
{
email: "1@gmail",
want: true,
},
}
for _, test := range tests {
result := ValidateEmail(test.email)
if result != test.want {
t.Errorf("Validate Email %s: got result %v, want %v.", test.email, result, test.want)
}
}
}
+9
View File
@@ -0,0 +1,9 @@
version: "3.0"
services:
memos:
image: neosmemo/memos:latest
container_name: memos
volumes:
- ~/.memos/:/var/opt/memos
ports:
- 5230:5230
+40
View File
@@ -0,0 +1,40 @@
# Development
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
1. It has no external dependency.
2. It requires zero config.
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
## Tech Stack
![tech-stack](https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png)
## Prerequisites
- [Go](https://golang.org/doc/install)
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
- [Node.js](https://nodejs.org/)
- [yarn](https://yarnpkg.com/getting-started/install)
## Steps
1. pull source code
```bash
git clone https://github.com/usememos/memos
```
2. start backend using air(with live reload)
```bash
air -c scripts/.air.toml
```
3. start frontend dev server
```bash
cd web && yarn && yarn dev
```
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
+33 -17
View File
@@ -1,32 +1,48 @@
module github.com/usememos/memos
go 1.17
go 1.19
require github.com/mattn/go-sqlite3 v1.14.9
require github.com/google/uuid v1.3.0
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220728030405-41545e8bf201
)
require (
github.com/gorilla/context v1.1.1 // indirect
github.com/labstack/echo/v4 v4.6.3
github.com/labstack/gommon v0.3.1 // indirect
)
require github.com/labstack/echo/v4 v4.9.0
require (
github.com/VictoriaMetrics/fastcache v1.10.0
github.com/gorilla/feeds v1.1.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.12.0
github.com/labstack/echo-contrib v0.13.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require github.com/segmentio/analytics-go v3.1.0+incompatible
+47 -569
View File
@@ -1,613 +1,91 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs=
github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/casbin/casbin/v2 v2.40.6/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY=
github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-contrib v0.12.0 h1:NPr1ez+XUa5s/4LujEon+32Bxg5DO6EKSW/va06pmLc=
github.com/labstack/echo-contrib v0.12.0/go.mod h1:kR62TbwsBgmpV2HVab5iQRsQtLuhPyGqCBee88XRc4M=
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac=
github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/labstack/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok3aENVKA=
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+98
View File
@@ -0,0 +1,98 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
type HTMLMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
}
func GetHTMLMeta(urlStr string) (*HTMLMeta, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if mediatype != "text/html" {
return nil, fmt.Errorf("Wrong website mediatype")
}
htmlMeta := extractHTMLMeta(response.Body)
return htmlMeta, nil
}
func extractHTMLMeta(resp io.Reader) *HTMLMeta {
tokenizer := html.NewTokenizer(resp)
htmlMeta := new(HTMLMeta)
for {
tokenType := tokenizer.Next()
if tokenType == html.ErrorToken {
break
} else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken {
token := tokenizer.Token()
if token.DataAtom == atom.Body {
break
}
if token.DataAtom == atom.Title {
tokenizer.Next()
token := tokenizer.Token()
htmlMeta.Title = token.Data
} else if token.DataAtom == atom.Meta {
description, ok := extractMetaProperty(token, "description")
if ok {
htmlMeta.Description = description
}
ogTitle, ok := extractMetaProperty(token, "og:title")
if ok {
htmlMeta.Title = ogTitle
}
ogDescription, ok := extractMetaProperty(token, "og:description")
if ok {
htmlMeta.Description = ogDescription
}
ogImage, ok := extractMetaProperty(token, "og:image")
if ok {
htmlMeta.Image = ogImage
}
}
}
}
return htmlMeta
}
func extractMetaProperty(token html.Token, prop string) (content string, ok bool) {
content, ok = "", false
for _, attr := range token.Attr {
if attr.Key == "property" && attr.Val == prop {
ok = true
}
if attr.Key == "content" {
content = attr.Val
}
}
return content, ok
}
+28
View File
@@ -0,0 +1,28 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetHTMLMeta(t *testing.T) {
tests := []struct {
urlStr string
htmlMeta HTMLMeta
}{
{
urlStr: "https://www.bytebase.com/blog/sql-review-tool-for-devs",
htmlMeta: HTMLMeta{
Title: "The SQL Review Tool for Developers",
Description: "Reviewing SQL can be somewhat tedious, yet is essential to keep your database fleet reliable. At Bytebase, we are building a developer-first SQL review tool to empower the DevOps system.",
Image: "https://www.bytebase.com/static/blog/sql-review-tool-for-devs/dev-fighting-dba.webp",
},
},
}
for _, test := range tests {
metadata, err := GetHTMLMeta(test.urlStr)
require.NoError(t, err)
require.Equal(t, test.htmlMeta, *metadata)
}
}
+4
View File
@@ -0,0 +1,4 @@
// getter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package getter
+45
View File
@@ -0,0 +1,45 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type Image struct {
Blob []byte
Mediatype string
}
func GetImage(urlStr string) (*Image, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if !strings.HasPrefix(mediatype, "image/") {
return nil, fmt.Errorf("Wrong image mediatype")
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
image := &Image{
Blob: bodyBytes,
Mediatype: mediatype,
}
return image, nil
}
+21
View File
@@ -0,0 +1,21 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetImage(t *testing.T) {
tests := []struct {
urlStr string
}{
{
urlStr: "https://star-history.com/bytebase.webp",
},
}
for _, test := range tests {
_, err := GetImage(test.urlStr)
require.NoError(t, err)
}
}
+15
View File
@@ -0,0 +1,15 @@
package getter
import (
"mime"
"net/http"
)
func getMediatype(response *http.Response) (string, error) {
contentType := response.Header.Get("content-type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
return mediatype, nil
}
+6
View File
@@ -0,0 +1,6 @@
package metric
// Collector is the interface definition for metric collector.
type Collector interface {
Collect(metric *Metric) error
}
+7
View File
@@ -0,0 +1,7 @@
package metric
// Metric is the API message for metric.
type Metric struct {
Name string
Labels map[string]string
}
+42
View File
@@ -0,0 +1,42 @@
package segment
import (
"time"
"github.com/google/uuid"
"github.com/segmentio/analytics-go"
metric "github.com/usememos/memos/plugin/metrics"
)
var (
sessionUUID = uuid.NewString()
)
// collector is the metrics collector https://segment.com/.
type collector struct {
client analytics.Client
}
// NewCollector creates a new instance of segment.
func NewCollector(key string) metric.Collector {
client := analytics.New(key)
return &collector{
client: client,
}
}
// Collect will exec all the segment collector.
func (c *collector) Collect(metric *metric.Metric) error {
properties := analytics.NewProperties()
for key, value := range metric.Labels {
properties.Set(key, value)
}
return c.client.Enqueue(analytics.Track{
Event: string(metric.Name),
AnonymousId: sessionUUID,
Properties: properties,
Timestamp: time.Now().UTC(),
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+1 -1
View File
@@ -5,7 +5,7 @@ tmp_dir = ".air"
bin = "./.air/memos"
cmd = "go build -o ./.air/memos ./bin/server/main.go"
delay = 1000
exclude_dir = [".air", "web"]
exclude_dir = [".air", "web", "build"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
Regular → Executable
+7 -4
View File
@@ -1,10 +1,13 @@
# Usage: sh ./scripts/build.sh
#!/bin/bash
# Usage: ./scripts/build.sh
set -e
cd "$(dirname "$0")/../"
echo "Start building..."
echo "Start building backend..."
go build -o ./memos-build/memos ./bin/server/main.go
go build -o ./build/memos ./bin/server/main.go
echo "Build finished"
echo "Backend built!"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Usage: ./scripts/start.sh
set -e
cd "$(dirname "$0")/../"
air -c ./scripts/.air.toml
+121
View File
@@ -0,0 +1,121 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
var (
userIDContextKey = "user-id"
)
func getUserIDContextKey() string {
return userIDContextKey
}
func setUserSession(ctx echo.Context, user *api.User) error {
sess, _ := session.Get("memos_session", ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 30,
HttpOnly: true,
}
sess.Values[userIDContextKey] = user.ID
err := sess.Save(ctx.Request(), ctx.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func removeUserSession(ctx echo.Context) error {
sess, _ := session.Get("memos_session", ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
sess.Values[userIDContextKey] = nil
err := sess.Save(ctx.Request(), ctx.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
path := c.Path()
// Skip auth.
if common.HasPrefixes(path, "/api/auth") {
return next(c)
}
{
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user != nil {
// Stores userID into context.
c.Set(getUserIDContextKey(), user.ID)
return next(c)
}
}
}
{
sess, _ := session.Get("memos_session", c)
userIDValue := sess.Values[userIDContextKey]
if userIDValue != nil {
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user != nil {
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
}
c.Set(getUserIDContextKey(), userID)
}
}
}
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id", "/api/memo/all", "/api/memo/:memoId", "/api/memo/amount") && c.Request().Method == http.MethodGet {
return next(c)
}
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
}
userID := c.Get(getUserIDContextKey())
if userID == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
return next(c)
}
}
+61 -33
View File
@@ -7,6 +7,7 @@ import (
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -14,22 +15,23 @@ import (
func (s *Server) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/signin", func(c echo.Context) error {
ctx := c.Request().Context()
signin := &api.Signin{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
userFind := &api.UserFind{
Email: &signin.Email,
Username: &signin.Username,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", signin.Username)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with username %s", signin.Username))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
@@ -41,6 +43,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed in",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
@@ -50,41 +55,66 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
})
g.POST("/auth/logout", func(c echo.Context) error {
ctx := c.Request().Context()
err := removeUserSession(c)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user logout",
})
c.Response().WriteHeader(http.StatusOK)
return nil
})
g.POST("/auth/signup", func(c echo.Context) error {
// Don't allow to signup by this api if site host existed.
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
hostUser, err := s.Store.FindUser(&hostUserFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
ctx := c.Request().Context()
signup := &api.Signup{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
}
// Validate signup form.
// We can do stricter checks later.
if len(signup.Email) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
if len(signup.Password) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 6.")
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if signup.Role == api.Host && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
systemSettingAllowSignUpName := api.SystemSettingAllowSignUpName
allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: &systemSettingAllowSignUpName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
userCreate := &api.UserCreate{
Username: signup.Username,
Role: api.Role(signup.Role),
Nickname: signup.Username,
Password: signup.Password,
OpenID: common.GenUUID(),
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
@@ -92,17 +122,15 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate := &api.UserCreate{
Email: signup.Email,
Role: api.Role(signup.Role),
Name: signup.Name,
PasswordHash: string(passwordHash),
OpenID: common.GenUUID(),
}
user, err := s.Store.CreateUser(userCreate)
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed up",
})
err = setUserSession(c, user)
if err != nil {
-119
View File
@@ -1,119 +0,0 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
var (
userIDContextKey = "user-id"
)
func getUserIDContextKey() string {
return userIDContextKey
}
func setUserSession(c echo.Context, user *api.User) error {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 1000 * 3600 * 24 * 30,
HttpOnly: true,
}
sess.Values[userIDContextKey] = user.ID
err := sess.Save(c.Request(), c.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func removeUserSession(c echo.Context) error {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
sess.Values[userIDContextKey] = nil
err := sess.Save(c.Request(), c.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
// Use session to store user.id.
func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Skip auth for some paths.
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status", "/api/user/:userId") {
return next(c)
}
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user != nil {
// Stores userID into context.
c.Set(getUserIDContextKey(), user.ID)
return next(c)
}
}
if common.HasPrefixes(c.Path(), "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
}
sess, err := session.Get("session", c)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session").SetInternal(err)
}
userIDValue := sess.Values[userIDContextKey]
if userIDValue == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing userID in session")
}
userID, err := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to malformatted user id in the session.").SetInternal(err)
}
// Even if there is no error, we still need to make sure the user still exists.
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Not found user ID: %d", userID))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
}
// Stores userID into context.
c.Set(getUserIDContextKey(), userID)
return next(c)
}
}
+17 -3
View File
@@ -12,8 +12,8 @@ import (
//go:embed dist
var embeddedFiles embed.FS
func getFileSystem() http.FileSystem {
fs, err := fs.Sub(embeddedFiles, "dist")
func getFileSystem(path string) http.FileSystem {
fs, err := fs.Sub(embeddedFiles, path)
if err != nil {
panic(err)
}
@@ -22,8 +22,22 @@ func getFileSystem() http.FileSystem {
}
func embedFrontend(e *echo.Echo) {
// Use echo static middleware to serve the built dist folder
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem(),
Filesystem: getFileSystem("dist"),
}))
g := e.Group("assets")
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
return next(c)
}
})
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist/assets"),
}))
}
+72
View File
@@ -0,0 +1,72 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
getter "github.com/usememos/memos/plugin/http_getter"
metric "github.com/usememos/memos/plugin/metrics"
)
func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
g.GET("/get/httpmeta", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
htmlMeta, err := getter.GetHTMLMeta(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "httpmeta",
},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(htmlMeta)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode website HTML meta").SetInternal(err)
}
return nil
})
g.GET("/get/image", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
image, err := getter.GetImage(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "image",
},
})
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(image.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
}
return nil
})
}
+378 -54
View File
@@ -4,33 +4,80 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) registerMemoRoutes(g *echo.Group) {
g.POST("/memo", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoCreate := &api.MemoCreate{
CreatorID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
}
if memoCreate.Visibility == nil || *memoCreate.Visibility == "" {
private := api.Privite
memoCreate.Visibility = &private
if memoCreate.Content == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Memo content shouldn't be empty")
}
memo, err := s.Store.CreateMemo(memoCreate)
if memoCreate.Visibility == "" {
userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
UserID: userID,
Key: &userSettingMemoVisibilityKey,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
}
if userMemoVisibilitySetting != nil {
memoVisibility := api.Private
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
}
memoCreate.Visibility = memoVisibility
} else {
// Private is the default memo visibility.
memoCreate.Visibility = api.Private
}
}
memo, err := s.Store.CreateMemo(ctx, memoCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "memo created",
})
for _, resourceID := range memoCreate.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
MemoID: memo.ID,
ResourceID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
@@ -40,23 +87,53 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
})
g.PATCH("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
currentTs := time.Now().Unix()
memoPatch := &api.MemoPatch{
ID: memoID,
ID: memoID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
}
memo, err := s.Store.PatchMemo(memoPatch)
memo, err := s.Store.PatchMemo(ctx, memoPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
}
for _, resourceID := range memoPatch.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
MemoID: memo.ID,
ResourceID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
@@ -65,24 +142,24 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
})
g.GET("/memo", func(c echo.Context) error {
ctx := c.Request().Context()
memoFind := &api.MemoFind{}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
} else {
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.CreatorID = &userID
}
// Only can get PUBLIC memos in visitor mode
_, ok := c.Get(getUserIDContextKey()).(int)
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
publicVisibility := api.Public
memoFind.Visibility = &publicVisibility
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if memoFind.CreatorID == nil {
memoFind.CreatorID = &currentUserID
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
}
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
@@ -99,6 +176,14 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilityListStr := c.QueryParam("visibility")
if visibilityListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilityListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
}
@@ -106,25 +191,215 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoFind.Offset = offset
}
list, err := s.Store.FindMemoList(memoFind)
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
var pinnedMemoList []*api.Memo
var unpinnedMemoList []*api.Memo
for _, memo := range list {
if memo.Pinned {
pinnedMemoList = append(pinnedMemoList, memo)
} else {
unpinnedMemoList = append(unpinnedMemoList, memo)
}
}
sort.Slice(pinnedMemoList, func(i, j int) bool {
return pinnedMemoList[i].DisplayTs > pinnedMemoList[j].DisplayTs
})
sort.Slice(unpinnedMemoList, func(i, j int) bool {
return unpinnedMemoList[i].DisplayTs > unpinnedMemoList[j].DisplayTs
})
memoList := []*api.Memo{}
memoList = append(memoList, pinnedMemoList...)
memoList = append(memoList, unpinnedMemoList...)
if memoFind.Limit != 0 {
memoList = memoList[memoFind.Offset:common.Min(len(memoList), memoFind.Offset+memoFind.Limit)]
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memoList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
}
return nil
})
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
g.GET("/memo/amount", func(c echo.Context) error {
ctx := c.Request().Context()
normalRowStatus := api.Normal
memoFind := &api.MemoFind{
RowStatus: &normalRowStatus,
}
if userID, err := strconv.Atoi(c.QueryParam("userId")); err == nil {
memoFind.CreatorID = &userID
}
memoList, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(len(memoList))); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo amount").SetInternal(err)
}
return nil
})
g.GET("/memo/stats", func(c echo.Context) error {
ctx := c.Request().Context()
normalStatus := api.Normal
memoFind := &api.MemoFind{
RowStatus: &normalStatus,
}
if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &creatorID
}
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if *memoFind.CreatorID != currentUserID {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Private}
}
}
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
displayTsList := []int64{}
for _, memo := range list {
displayTsList = append(displayTsList, memo.DisplayTs)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(displayTsList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo stats response").SetInternal(err)
}
return nil
})
g.GET("/memo/all", func(c echo.Context) error {
ctx := c.Request().Context()
memoFind := &api.MemoFind{}
_, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
pinnedStr := c.QueryParam("pinned")
if pinnedStr != "" {
pinned := pinnedStr == "true"
memoFind.Pinned = &pinned
}
tag := c.QueryParam("tag")
if tag != "" {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilityListStr := c.QueryParam("visibility")
if visibilityListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilityListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
}
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
memoFind.Offset = offset
}
// Only fetch normal status memos.
normalStatus := api.Normal
memoFind.RowStatus = &normalStatus
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
}
sort.Slice(list, func(i, j int) bool {
return list[i].DisplayTs > list[j].DisplayTs
})
if memoFind.Limit != 0 {
list = list[memoFind.Offset:common.Min(len(list), memoFind.Offset+memoFind.Limit)]
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID := c.Get(getUserIDContextKey()).(int)
memoFind := &api.MemoFind{
ID: &memoID,
}
memo, err := s.Store.FindMemo(ctx, memoFind)
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if memo.Visibility == api.Private {
if !ok || memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
}
} else if memo.Visibility == api.Protected {
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
}
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
MemoID: memoID,
UserID: userID,
@@ -133,12 +408,12 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
err = s.Store.UpsertMemoOrganizer(memoOrganizerUpsert)
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err := s.Store.FindMemo(&api.MemoFind{
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
ID: &memoID,
})
if err != nil {
@@ -156,66 +431,115 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
g.POST("/memo/:memoId/resource", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
currentTs := time.Now().Unix()
memoResourceUpsert := &api.MemoResourceUpsert{
MemoID: memoID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
}
memo, err := s.Store.FindMemo(memoFind)
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &memoResourceUpsert.ResourceID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId", func(c echo.Context) error {
g.GET("/memo/:memoId/resource", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoDelete := &api.MemoDelete{
ID: memoID,
resourceFind := &api.ResourceFind{
MemoID: &memoID,
}
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
err = s.Store.DeleteMemo(memoDelete)
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resourceList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
memoResourceDelete := &api.MemoResourceDelete{
MemoID: &memoID,
ResourceID: &resourceID,
}
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.GET("/memo/amount", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
normalRowStatus := api.Normal
memoFind := &api.MemoFind{
CreatorID: &userID,
RowStatus: &normalRowStatus,
g.DELETE("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoList, err := s.Store.FindMemoList(memoFind)
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(len(memoList))); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo amount").SetInternal(err)
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
return nil
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
memoDelete := &api.MemoDelete{
ID: memoID,
}
if err := s.Store.DeleteMemo(ctx, memoDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}
+55
View File
@@ -0,0 +1,55 @@
package server
import (
"context"
"fmt"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/plugin/metrics/segment"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
// MetricCollector is the metric collector.
type MetricCollector struct {
Collector metric.Collector
Enabled bool
Profile *profile.Profile
Store *store.Store
}
const (
segmentMetricWriteKey = "fTn5BumOkj352n3TGw9tu0ARH2dOkcoQ"
)
func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricCollector {
c := segment.NewCollector(segmentMetricWriteKey)
return MetricCollector{
Collector: c,
Enabled: true,
Profile: profile,
Store: store,
}
}
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
if !mc.Enabled {
return
}
if mc.Profile.Mode == "dev" {
return
}
if metric.Labels == nil {
metric.Labels = map[string]string{}
}
metric.Labels["version"] = version.GetCurrentVersion(mc.Profile.Mode)
err := mc.Collector.Collect(metric)
if err != nil {
fmt.Printf("Failed to request segment, error: %+v\n", err)
}
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/usememos/memos/common"
"github.com/usememos/memos/server/version"
)
// Profile is the configuration to start main server.
@@ -45,7 +45,7 @@ func checkDSN(dataDir string) (string, error) {
}
// GetDevProfile will return a profile for dev or prod.
func GetProfile() *Profile {
func GetProfile() (*Profile, error) {
profile := Profile{}
flag.StringVar(&profile.Mode, "mode", "dev", "mode of server")
flag.IntVar(&profile.Port, "port", 8080, "port of server")
@@ -63,12 +63,12 @@ func GetProfile() *Profile {
dataDir, err := checkDSN(profile.Data)
if err != nil {
fmt.Printf("Failed to check dsn: %s, err: %+v\n", dataDir, err)
os.Exit(1)
return nil, err
}
profile.Data = dataDir
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
profile.Version = common.GetCurrentVersion(profile.Mode)
profile.Version = version.GetCurrentVersion(profile.Mode)
return &profile
return &profile, nil
}
+142 -14
View File
@@ -3,26 +3,41 @@ package server
import (
"encoding/json"
"fmt"
"io/ioutil"
"html"
"io"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
const (
// The max file size is 32MB.
maxFileSize = (32 * 8) << 20
)
func (s *Server) registerResourceRoutes(g *echo.Group) {
g.POST("/resource", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
err := c.Request().ParseMultipartForm(64 << 20)
if err != nil {
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
}
if file == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
}
@@ -35,7 +50,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
defer src.Close()
fileBytes, err := ioutil.ReadAll(src)
fileBytes, err := io.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
}
@@ -48,10 +63,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
CreatorID: userID,
}
resource, err := s.Store.CreateResource(resourceCreate)
resource, err := s.Store.CreateResource(ctx, resourceCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "resource created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
@@ -61,15 +79,29 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceFind := &api.ResourceFind{
CreatorID: &userID,
}
list, err := s.Store.FindResourceList(resourceFind)
list, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
for _, resource := range list {
memoResourceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
ResourceID: &resource.ID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
}
resource.LinkedMemoAmount = len(memoResourceList)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
@@ -78,17 +110,21 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
userID := c.Get(getUserIDContextKey()).(int)
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
resource, err := s.Store.FindResource(resourceFind)
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
@@ -101,17 +137,21 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
userID := c.Get(getUserIDContextKey()).(int)
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
resource, err := s.Store.FindResource(resourceFind)
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
@@ -121,23 +161,111 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
}
return nil
})
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, "Not find resource").SetInternal(err)
}
resourceDelete := &api.ResourceDelete{
ID: resourceID,
}
if err := s.Store.DeleteResource(resourceDelete); err != nil {
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.PATCH("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
if _, err := s.Store.FindResource(ctx, resourceFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
currentTs := time.Now().Unix()
resourcePatch := &api.ResourcePatch{
ID: resourceID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
}
resource, err := s.Store.PatchResource(ctx, resourcePatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
}
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := html.UnescapeString(c.Param("filename"))
resourceFind := &api.ResourceFind{
ID: &resourceID,
Filename: &filename,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
}
return nil
})
}
+72
View File
@@ -0,0 +1,72 @@
package server
import (
"net/http"
"strconv"
"time"
"github.com/gorilla/feeds"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
)
func (s *Server) registerRSSRoutes(g *echo.Group) {
g.GET("/u/:id/rss.xml", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
}
normalStatus := api.Normal
memoFind := api.MemoFind{
CreatorID: &id,
RowStatus: &normalStatus,
VisibilityList: []api.Visibility{
api.Public,
},
}
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
userFind := api.UserFind{
ID: &id,
}
user, err := s.Store.FindUser(ctx, &userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
feed := &feeds.Feed{
Title: "Memos",
Link: &feeds.Link{Href: baseURL},
Description: "Memos",
Author: &feeds.Author{Name: user.Username},
Created: time.Now(),
}
feed.Items = make([]*feeds.Item, len(memoList))
for i, memo := range memoList {
feed.Items[i] = &feeds.Item{
Title: user.Username + "-memos-" + strconv.Itoa(memo.ID),
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
Description: memo.Content,
Created: time.Unix(memo.CreatedTs, 0),
}
}
rss, err := feed.ToRss()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
rssPrefix := `<?xml version="1.0" encoding="UTF-8"?>`
return c.XMLBlob(http.StatusOK, []byte(rss[len(rssPrefix):]))
})
}
+16 -4
View File
@@ -17,6 +17,8 @@ import (
type Server struct {
e *echo.Echo
Collector *MetricCollector
Profile *profile.Profile
Store *store.Store
@@ -29,9 +31,13 @@ func NewServer(profile *profile.Profile) *Server {
e.HidePort = true
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${method} ${uri} ${status}\n",
Format: `{"time":"${time_rfc3339}",` +
`"method":"${method}","uri":"${uri}",` +
`"status":${status},"error":"${error}"}` + "\n",
}))
e.Use(middleware.CORS())
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Skipper: middleware.DefaultSkipper,
ErrorMessage: "Request timeout",
@@ -52,13 +58,19 @@ func NewServer(profile *profile.Profile) *Server {
Profile: profile,
}
// Webhooks api skips auth checker.
rootGroup := e.Group("")
s.registerRSSRoutes(rootGroup)
webhookGroup := e.Group("/h")
s.registerWebhookRoutes(webhookGroup)
s.registerResourcePublicRoutes(webhookGroup)
publicGroup := e.Group("/o")
s.registerResourcePublicRoutes(publicGroup)
s.registerGetterPublicRoutes(publicGroup)
apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return BasicAuthMiddleware(s, next)
return aclMiddleware(s, next)
})
s.registerSystemRoutes(apiGroup)
s.registerAuthRoutes(apiGroup)
+27 -8
View File
@@ -5,15 +5,22 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) registerShortcutRoutes(g *echo.Group) {
g.POST("/shortcut", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
shortcutCreate := &api.ShortcutCreate{
CreatorID: userID,
}
@@ -21,10 +28,13 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
}
shortcut, err := s.Store.CreateShortcut(shortcutCreate)
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "shortcut created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
@@ -34,19 +44,22 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
currentTs := time.Now().Unix()
shortcutPatch := &api.ShortcutPatch{
ID: shortcutID,
ID: shortcutID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
}
shortcut, err := s.Store.PatchShortcut(shortcutPatch)
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
}
@@ -59,6 +72,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.GET("/shortcut", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutFind := &api.ShortcutFind{}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
@@ -72,7 +86,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
shortcutFind.CreatorID = &userID
}
list, err := s.Store.FindShortcutList(shortcutFind)
list, err := s.Store.FindShortcutList(ctx, shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)
}
@@ -85,6 +99,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.GET("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
@@ -93,7 +108,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
shortcutFind := &api.ShortcutFind{
ID: &shortcutID,
}
shortcut, err := s.Store.FindShortcut(shortcutFind)
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", *shortcutFind.ID)).SetInternal(err)
}
@@ -106,15 +121,19 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.DELETE("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutDelete := &api.ShortcutDelete{
ID: shortcutID,
ID: &shortcutID,
}
if err := s.Store.DeleteShortcut(shortcutDelete); err != nil {
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Shortcut ID not found: %d", shortcutID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
}
+106 -4
View File
@@ -3,8 +3,11 @@ package server
import (
"encoding/json"
"net/http"
"os"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
@@ -21,12 +24,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
})
g.GET("/status", func(c echo.Context) error {
ctx := c.Request().Context()
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
hostUser, err := s.Store.FindUser(&hostUserFind)
if err != nil {
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
@@ -36,8 +40,49 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
systemStatus := api.SystemStatus{
Host: hostUser,
Profile: s.Profile,
Host: hostUser,
Profile: *s.Profile,
DBSize: 0,
AllowSignUp: false,
AdditionalStyle: "",
AdditionalScript: "",
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
for _, systemSetting := range systemSettingList {
var value interface{}
err = json.Unmarshal([]byte(systemSetting.Value), &value)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if systemSetting.Name == api.SystemSettingAllowSignUpName {
systemStatus.AllowSignUp = value.(bool)
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
systemStatus.AdditionalScript = value.(string)
}
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user != nil && user.Role == api.Host {
fi, err := os.Stat(s.Profile.DSN)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read database fileinfo").SetInternal(err)
}
systemStatus.DBSize = fi.Size()
}
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
@@ -46,4 +91,61 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
return nil
})
g.POST("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "Current signin user not found")
} else if user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingUpsert := &api.SystemSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
}
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "system setting invalidate").SetInternal(err)
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, systemSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "systemSetting updated",
Labels: map[string]string{"field": string(systemSettingUpsert.Name)},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting response").SetInternal(err)
}
return nil
})
g.GET("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSettingList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting list response").SetInternal(err)
}
return nil
})
}
+33 -21
View File
@@ -12,8 +12,11 @@ import (
"github.com/labstack/echo/v4"
)
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`[^\S]#([^\s#]+?) `), regexp.MustCompile(` #([^\s#]+?) `)}
func (s *Server) registerTagRoutes(g *echo.Group) {
g.GET("/tag", func(c echo.Context) error {
ctx := c.Request().Context()
contentSearch := "#"
normalRowStatus := api.Normal
memoFind := api.MemoFind{
@@ -22,46 +25,38 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
} else {
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
}
memoFind.CreatorID = &userID
}
// Only can get PUBLIC memos in visitor mode
_, ok := c.Get(getUserIDContextKey()).(int)
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
publicVisibility := api.Public
memoFind.Visibility = &publicVisibility
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if memoFind.CreatorID == nil {
memoFind.CreatorID = &currentUserID
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
}
memoList, err := s.Store.FindMemoList(&memoFind)
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
tagMapSet := make(map[string]bool)
r, err := regexp.Compile("#(.+?) ")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile regexp").SetInternal(err)
}
for _, memo := range memoList {
for _, rawTag := range r.FindAllString(memo.Content, -1) {
tag := r.ReplaceAllString(rawTag, "$1")
for _, tag := range findTagListFromMemoContent(memo.Content) {
tagMapSet[tag] = true
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
@@ -71,3 +66,20 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
return nil
})
}
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
for _, tagRegexp := range tagRegexpList {
for _, rawTag := range tagRegexp.FindAllString(memoContent, -1) {
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
tagMapSet[tag] = true
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return tagList
}
+47
View File
@@ -0,0 +1,47 @@
package server
import (
"testing"
)
func TestFindTagListFromMemoContent(t *testing.T) {
tests := []struct {
memoContent string
want []string
}{
{
memoContent: "#tag1 ",
want: []string{"tag1"},
},
{
memoContent: "#tag1 #tag2 ",
want: []string{"tag1", "tag2"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 ",
want: []string{"tag1", "tag2", "tag3"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag3", "tag4"},
},
{
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
want: []string{"tag1", "tag3", "tag4"},
},
}
for _, test := range tests {
result := findTagListFromMemoContent(test.memoContent)
if len(result) != len(test.want) {
t.Errorf("Find tag list %s: got result %v, want %v.", test.memoContent, result, test.want)
}
}
}
+146 -55
View File
@@ -5,9 +5,11 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -15,21 +17,45 @@ import (
func (s *Server) registerUserRoutes(g *echo.Group) {
g.POST("/user", func(c echo.Context) error {
userCreate := &api.UserCreate{}
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
}
if currentUser.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Only Host user can create member.")
}
userCreate := &api.UserCreate{
OpenID: common.GenUUID(),
}
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(userCreate)
user, err := s.Store.CreateUser(ctx, userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
@@ -39,11 +65,17 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
})
g.GET("/user", func(c echo.Context) error {
userList, err := s.Store.FindUserList(&api.UserFind{})
ctx := c.Request().Context()
userList, err := s.Store.FindUserList(ctx, &api.UserFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
}
for _, user := range userList {
// data desensitize
user.OpenID = ""
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
@@ -51,13 +83,73 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
UserID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.POST("/user/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userSettingUpsert := &api.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
if err := userSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err)
}
userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err)
}
return nil
})
g.GET("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
}
user, err := s.Store.FindUser(&api.UserFind{
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &id,
})
if err != nil {
@@ -76,38 +168,41 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
userSessionID := c.Get(getUserIDContextKey())
if userSessionID == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userID := userSessionID.(int)
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(userFind)
g.PATCH("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
userID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &currentUserID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
} else if currentUser.Role != api.Host && currentUserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.PATCH("/user/me", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
currentTs := time.Now().Unix()
userPatch := &api.UserPatch{
ID: userID,
ID: userID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Email != nil && *userPatch.Email != "" && !common.ValidateEmail(*userPatch.Email) {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format")
}
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
@@ -123,11 +218,19 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
userPatch.OpenID = &openID
}
user, err := s.Store.PatchUser(userPatch)
user, err := s.Store.PatchUser(ctx, userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
UserID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
@@ -135,9 +238,13 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
g.PATCH("/user/:userId", func(c echo.Context) error {
currentUserID := c.Get(getUserIDContextKey()).(int)
currentUser, err := s.Store.FindUser(&api.UserFind{
g.DELETE("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &currentUserID,
})
if err != nil {
@@ -149,37 +256,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
}
userID, err := strconv.Atoi(c.Param("userId"))
userID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("userId"))).SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
}
userPatch := &api.UserPatch{
userDelete := &api.UserDelete{
ID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User ID not found: %d", userID))
}
passwordHashStr := string(passwordHash)
userPatch.PasswordHash = &passwordHashStr
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
}
user, err := s.Store.PatchUser(userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
return c.JSON(http.StatusOK, true)
})
}
@@ -1,4 +1,4 @@
package common
package version
import (
"strconv"
@@ -7,10 +7,10 @@ import (
// Version is the service current released version.
// Semantic versioning: https://semver.org/
var Version = "0.2.1"
var Version = "0.8.1"
// DevVersion is the service current development version.
var DevVersion = "0.2.1"
var DevVersion = "0.8.1"
func GetCurrentVersion(mode string) string {
if mode == "dev" {
@@ -27,6 +27,12 @@ func GetMinorVersion(version string) string {
return versionList[0] + "." + versionList[1]
}
func GetSchemaVersion(version string) string {
minorVersion := GetMinorVersion(version)
return minorVersion + ".0"
}
// convSemanticVersionToInt converts version string to int.
func convSemanticVersionToInt(version string) int {
versionList := strings.Split(version, ".")
-42
View File
@@ -1,42 +0,0 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/labstack/echo/v4"
)
func (s *Server) registerWebhookRoutes(g *echo.Group) {
g.GET("/test", func(c echo.Context) error {
return c.HTML(http.StatusOK, "<strong>Hello, World!</strong>")
})
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := c.Param("filename")
resourceFind := &api.ResourceFind{
ID: &resourceID,
Filename: &filename,
}
resource, err := s.Store.FindResource(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
}
return nil
})
}
+72
View File
@@ -0,0 +1,72 @@
package store
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"github.com/VictoriaMetrics/fastcache"
"github.com/usememos/memos/api"
)
var (
// 64 MiB.
cacheSize = 1024 * 1024 * 64
_ api.CacheService = (*CacheService)(nil)
)
// CacheService implements a cache.
type CacheService struct {
cache *fastcache.Cache
}
// NewCacheService creates a cache service.
func NewCacheService() *CacheService {
return &CacheService{
cache: fastcache.New(cacheSize),
}
}
// FindCache finds the value in cache.
func (s *CacheService) FindCache(namespace api.CacheNamespace, id int, entry interface{}) (bool, error) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
buf2, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
dec := gob.NewDecoder(bytes.NewReader(buf2))
if err := dec.Decode(entry); err != nil {
return false, fmt.Errorf("failed to decode entry for cache namespace: %s, error: %w", namespace, err)
}
return true, nil
}
return false, nil
}
// UpsertCache upserts the value to cache.
func (s *CacheService) UpsertCache(namespace api.CacheNamespace, id int, entry interface{}) error {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
var buf2 bytes.Buffer
enc := gob.NewEncoder(&buf2)
if err := enc.Encode(entry); err != nil {
return fmt.Errorf("failed to encode entry for cache namespace: %s, error: %w", namespace, err)
}
s.cache.Set(append([]byte(namespace), buf1...), buf2.Bytes())
return nil
}
// DeleteCache deletes the cache.
func (s *CacheService) DeleteCache(namespace api.CacheNamespace, id int) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
_, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
s.cache.Del(append([]byte(namespace), buf1...))
}
}
+86 -99
View File
@@ -1,21 +1,19 @@
package db
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"regexp"
"sort"
"time"
"github.com/usememos/memos/common"
"github.com/usememos/memos/server/profile"
_ "github.com/mattn/go-sqlite3"
"github.com/usememos/memos/server/version"
)
//go:embed migration
@@ -38,112 +36,108 @@ func NewDB(profile *profile.Profile) *DB {
return db
}
func (db *DB) Open() (err error) {
func (db *DB) Open(ctx context.Context) (err error) {
// Ensure a DSN is set before attempting to open the database.
if db.profile.DSN == "" {
return fmt.Errorf("dsn required")
}
// Connect to the database.
sqlDB, err := sql.Open("sqlite3", db.profile.DSN)
// Connect to the database without foreign_key.
sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=0")
if err != nil {
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
}
db.Db = sqlDB
// If mode is dev, we should migrate and seed the database.
if db.profile.Mode == "dev" {
if err := db.applyLatestSchema(); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
if err := db.seed(); err != nil {
return fmt.Errorf("failed to seed: %w", err)
// In dev mode, we should migrate and seed the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
if err := db.seed(ctx); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
}
} else {
// If db file not exists, we should migrate the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
err := db.applyLatestSchema()
if err != nil {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
} else {
err := db.createMigrationHistoryTable()
}
currentVersion := version.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
if err != nil {
return fmt.Errorf("failed to find migration history, err: %w", err)
}
if migrationHistory == nil {
if _, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
Version: currentVersion,
}); err != nil {
return fmt.Errorf("failed to upsert migration history, err: %w", err)
}
return nil
}
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := os.ReadFile(db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to create migration_history table: %w", err)
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
currentVersion := common.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := findMigrationHistory(db.Db, &MigrationHistoryFind{})
if err != nil {
return err
}
if migrationHistory == nil {
migrationHistory, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
Version: currentVersion,
})
if err != nil {
return err
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
println("succeed to copy a backup database file")
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := ioutil.ReadFile(db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := ioutil.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
println("succeed to copy a backup database file")
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
}
}
println("end migrate")
println("end migrate")
// remove the created backup db file after migrate succeed
if err := os.Remove(backupDBFilePath); err != nil {
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
}
// remove the created backup db file after migrate succeed
if err := os.Remove(backupDBFilePath); err != nil {
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
}
}
}
return err
return nil
}
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (db *DB) applyLatestSchema() error {
latestSchemaPath := fmt.Sprintf("%s/%s", "migration", latestSchemaFileName)
func (db *DB) applyLatestSchema(ctx context.Context) error {
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
buf, err := migrationFS.ReadFile(latestSchemaPath)
if err != nil {
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
}
stmt := string(buf)
if err := db.execute(stmt); err != nil {
if err := db.execute(ctx, stmt); err != nil {
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
}
return nil
}
func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration", minorVersion))
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
if err != nil {
return err
return fmt.Errorf("failed to read ddl files, err: %w", err)
}
sort.Strings(filenames)
@@ -157,26 +151,33 @@ func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
}
stmt := string(buf)
migrationStmt += stmt
if err := db.execute(stmt); err != nil {
if err := db.execute(ctx, stmt); err != nil {
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
}
}
// upsert the newest version to migration_history
if _, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
Version: minorVersion + ".0",
}); err != nil {
return err
}
return nil
}
func (db *DB) seed() error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// upsert the newest version to migration_history
version := minorVersion + ".0"
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
Version: version,
}); err != nil {
return fmt.Errorf("failed to upsert migration history with version: %s, err: %w", version, err)
}
return tx.Commit()
}
func (db *DB) seed(ctx context.Context) error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
if err != nil {
return fmt.Errorf("failed to read seed files, err: %w", err)
}
sort.Strings(filenames)
@@ -187,30 +188,30 @@ func (db *DB) seed() error {
return fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
}
stmt := string(buf)
if err := db.execute(stmt); err != nil {
if err := db.execute(ctx, stmt); err != nil {
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
}
}
return nil
}
// excecute runs a single SQL statement within a transaction.
func (db *DB) execute(stmt string) error {
// execute runs a single SQL statement within a transaction.
func (db *DB) execute(ctx context.Context, stmt string) error {
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(stmt); err != nil {
return err
if _, err := tx.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("failed to execute statement, err: %w", err)
}
return tx.Commit()
}
// minorDirRegexp is a regular expression for minor version directory.
var minorDirRegexp = regexp.MustCompile(`^migration/[0-9]+\.[0-9]+$`)
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
func getMinorVersionList() []string {
minorVersionList := []string{}
@@ -232,17 +233,3 @@ func getMinorVersionList() []string {
return minorVersionList
}
// createMigrationHistoryTable creates the migration_history table if it doesn't exist.
func (db *DB) createMigrationHistoryTable() error {
if err := createTable(db.Db, `
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
`); err != nil {
return err
}
return nil
}
@@ -1 +0,0 @@
ALTER TABLE memo ADD visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';
+88
View File
@@ -0,0 +1,88 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
@@ -4,8 +4,7 @@ PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE
user RENAME TO _user_old;
ALTER TABLE user RENAME TO _user_old;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -0,0 +1 @@
ALTER TABLE memo ADD COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';
@@ -0,0 +1,37 @@
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO memo (
id, creator_id, created_ts, updated_ts,
row_status, content, visibility
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
PRAGMA foreign_keys = on;
@@ -0,0 +1,9 @@
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
@@ -1,16 +1,14 @@
-- drop all tables
DROP TABLE IF EXISTS `memo_organizer`;
DROP TABLE IF EXISTS `memo`;
DROP TABLE IF EXISTS `shortcut`;
DROP TABLE IF EXISTS `resource`;
DROP TABLE IF EXISTS `user`;
PRAGMA foreign_keys=off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
-- allowed row status are 'NORMAL', 'ARCHIVED'.
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
@@ -19,10 +17,11 @@ CREATE TABLE user (
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('user', 100);
INSERT INTO user SELECT * FROM _user_old;
DROP TABLE IF EXISTS _user_old;
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
@@ -36,6 +35,10 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -44,14 +47,15 @@ CREATE TABLE memo (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo', 1000);
INSERT INTO memo SELECT * FROM _memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
@@ -65,6 +69,10 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -76,10 +84,13 @@ CREATE TABLE memo_organizer (
UNIQUE(memo_id, user_id)
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo_organizer', 1000);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
@@ -93,10 +104,11 @@ CREATE TABLE shortcut (
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('shortcut', 10000);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
@@ -110,6 +122,10 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -117,16 +133,17 @@ CREATE TABLE resource (
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB NOT NULL,
blob BLOB DEFAULT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('resource', 10000);
INSERT INTO resource SELECT * FROM _resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
@@ -139,3 +156,22 @@ SET
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
PRAGMA foreign_keys=on;
@@ -0,0 +1,10 @@
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE,
UNIQUE(memo_id, resource_id)
);
@@ -0,0 +1,7 @@
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
@@ -0,0 +1 @@
ALTER TABLE resource ADD COLUMN external_link TEXT NOT NULL DEFAULT '';
@@ -0,0 +1,55 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
UPDATE
ON `user` FOR EACH ROW BEGIN
UPDATE
`user`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
UPDATE
ON `memo` FOR EACH ROW BEGIN
UPDATE
`memo`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
UPDATE
ON `shortcut` FOR EACH ROW BEGIN
UPDATE
`shortcut`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
UPDATE
ON `resource` FOR EACH ROW BEGIN
UPDATE
`resource`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
@@ -0,0 +1,147 @@
PRAGMA foreign_keys=off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user SELECT * FROM _user_old;
DROP TABLE IF EXISTS _user_old;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
INSERT INTO memo SELECT * FROM _memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
INSERT INTO resource (
id, creator_id, created_ts, updated_ts,
filename, blob, external_link, type,
size
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
DROP TABLE IF EXISTS _memo_resource_old;
ALTER TABLE memo_resource RENAME TO _memo_resource_old;
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
INSERT INTO memo_resource SELECT * FROM _memo_resource_old;
DROP TABLE IF EXISTS _memo_resource_old;
@@ -0,0 +1,4 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
@@ -0,0 +1,5 @@
-- migration_history
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -0,0 +1,41 @@
-- add column username TEXT NOT NULL UNIQUE
-- rename column name to nickname
-- add role `ADMIN`
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user (
id, created_ts, updated_ts, row_status,
username, role, email, nickname, password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
@@ -0,0 +1,88 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
+52 -29
View File
@@ -1,6 +1,7 @@
package db
import (
"context"
"database/sql"
"strings"
)
@@ -18,23 +19,62 @@ type MigrationHistoryFind struct {
Version *string
}
func findMigrationHistoryList(db *sql.DB, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
list, err := findMigrationHistoryList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
migrationHistory := list[0]
return migrationHistory, nil
}
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
migrationHistory, err := upsertMigrationHistory(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return migrationHistory, nil
}
func findMigrationHistoryList(ctx context.Context, tx *sql.Tx, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Version; v != nil {
where, args = append(where, "version = ?"), append(args, *v)
}
rows, err := db.Query(`
query := `
SELECT
version,
created_ts
FROM
migration_history
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY version DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
@@ -53,24 +93,15 @@ func findMigrationHistoryList(db *sql.DB, find *MigrationHistoryFind) ([]*Migrat
migrationHistoryList = append(migrationHistoryList, &migrationHistory)
}
return migrationHistoryList, nil
}
func findMigrationHistory(db *sql.DB, find *MigrationHistoryFind) (*MigrationHistory, error) {
list, err := findMigrationHistoryList(db, find)
if err != nil {
if err := rows.Err(); err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
} else {
return list[0], nil
}
return migrationHistoryList, nil
}
func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
row, err := db.Query(`
func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
query := `
INSERT INTO migration_history (
version
)
@@ -79,17 +110,9 @@ func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*Migrat
SET
version=EXCLUDED.version
RETURNING version, created_ts
`,
upsert.Version,
)
if err != nil {
return nil, err
}
defer row.Close()
row.Next()
`
var migrationHistory MigrationHistory
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, upsert.Version).Scan(
&migrationHistory.Version,
&migrationHistory.CreatedTs,
); err != nil {
+35 -7
View File
@@ -1,18 +1,20 @@
INSERT INTO
user (
`id`,
`email`,
`username`,
`role`,
`name`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
101,
'demo@usememos.com',
'demohero',
'HOST',
'Demo Host',
'demo@usememos.com',
'Demo Hero',
'demo_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
@@ -21,19 +23,45 @@ VALUES
INSERT INTO
user (
`id`,
`email`,
`username`,
`role`,
`name`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
102,
'jack@usememos.com',
'jack',
'USER',
'jack@usememos.com',
'Jack',
'jack_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
user (
`id`,
`row_status`,
`username`,
`role`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
103,
'ARCHIVED',
'bob',
'USER',
'bob@usememos.com',
'Bob',
'bob_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
+27 -22
View File
@@ -7,28 +7,29 @@ INSERT INTO
VALUES
(
1001,
"#Hello 👋 Welcome to memos.
And here is old Jack's Page: [/u/102](/u/102)",
101
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`
)
VALUES
(
1002,
'#TODO
- [ ] Take more photos about **🌄 sunset**;
- [x] Clean the room;
- [x] Read *📖 The Little Prince*;
(👆 click to toggle status)',
"#Hello 👋 Welcome to memos.",
101
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`,
`visibility`
)
VALUES
(
1002,
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [x] Clean the room;
- [ ] Read *📖 The Little Prince*;
(👆 click to toggle status)',
101,
'PROTECTED'
);
INSERT INTO
memo (
`id`,
@@ -39,7 +40,9 @@ INSERT INTO
VALUES
(
1003,
'好好学习,天天向上。🤜🤛',
"**Bytebase** - An open source Database CI/CD for DevOps teams.
![](https://star-history.com/bytebase.webp)
🌐 [Source code](https://github.com/bytebase/bytebase)",
101,
'PUBLIC'
);
@@ -48,7 +51,8 @@ INSERT INTO
memo (
`id`,
`content`,
`creator_id`
`creator_id`,
`visibility`
)
VALUES
(
@@ -59,7 +63,8 @@ VALUES
- [ ] Watch *👦 The Boys*;
(👆 click to toggle status)
',
102
102,
'PROTECTED'
);
INSERT INTO
+13
View File
@@ -10,3 +10,16 @@ VALUES
101,
1
);
INSERT INTO
memo_organizer (
`memo_id`,
`user_id`,
`pinned`
)
VALUES
(
1003,
101,
1
);
+5 -3
View File
@@ -1,10 +1,12 @@
INSERT INTO
shortcut (
`title`,
`creator_id`
`creator_id`,
`payload`
)
VALUES
(
'All my memos',
101
'inbox',
101,
'[{"type":"TYPE","value":{"operator":"IS","value":"NOT_TAGGED"},"relation":"AND"}]'
);
+12
View File
@@ -0,0 +1,12 @@
INSERT INTO
system_setting (
`name`,
`value`,
`description`
)
VALUES
(
'allowSignUp',
'true',
''
);
-66
View File
@@ -1,66 +0,0 @@
package db
import (
"database/sql"
"strings"
)
type Table struct {
Name string
SQL string
}
//lint:ignore U1000 Ignore unused function temporarily for debugging
func findTable(db *sql.DB, tableName string) (*Table, error) {
where, args := []string{"1 = 1"}, []interface{}{}
where, args = append(where, "type = ?"), append(args, "table")
where, args = append(where, "name = ?"), append(args, tableName)
rows, err := db.Query(`
SELECT
tbl_name,
sql
FROM sqlite_schema
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
tableList := make([]*Table, 0)
for rows.Next() {
var table Table
if err := rows.Scan(
&table.Name,
&table.SQL,
); err != nil {
return nil, err
}
tableList = append(tableList, &table)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(tableList) == 0 {
return nil, nil
} else {
return tableList[0], nil
}
}
func createTable(db *sql.DB, sql string) error {
result, err := db.Exec(sql)
if err != nil {
return err
}
_, err = result.RowsAffected()
return err
}
+189 -85
View File
@@ -1,7 +1,9 @@
package store
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
@@ -40,16 +42,71 @@ func (raw *memoRaw) toMemo() *api.Memo {
// Domain specific fields
Content: raw.Content,
Visibility: raw.Visibility,
DisplayTs: raw.CreatedTs,
}
}
func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
memoRaw, err := createMemoRaw(s.db, create)
func (s *Store) ComposeMemo(ctx context.Context, memo *api.Memo) (*api.Memo, error) {
memoOrganizer, err := s.FindMemoOrganizer(ctx, &api.MemoOrganizerFind{
MemoID: memo.ID,
UserID: memo.CreatorID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return nil, err
} else if memoOrganizer != nil {
memo.Pinned = memoOrganizer.Pinned
}
if err = s.ComposeMemoCreator(ctx, memo); err != nil {
return nil, err
}
if err = s.ComposeMemoResourceList(ctx, memo); err != nil {
return nil, err
}
memoDisplayTsOptionKey := api.UserSettingMemoDisplayTsOptionKey
memoDisplayTsOptionSetting, err := s.FindUserSetting(ctx, &api.UserSettingFind{
UserID: memo.CreatorID,
Key: &memoDisplayTsOptionKey,
})
if err != nil {
return nil, err
}
memoDisplayTsOptionValue := "created_ts"
if memoDisplayTsOptionSetting != nil {
err = json.Unmarshal([]byte(memoDisplayTsOptionSetting.Value), &memoDisplayTsOptionValue)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal user setting memo display ts option value")
}
}
if memoDisplayTsOptionValue == "updated_ts" {
memo.DisplayTs = memo.UpdatedTs
}
return memo, nil
}
func (s *Store) CreateMemo(ctx context.Context, create *api.MemoCreate) (*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRaw, err := createMemoRaw(ctx, tx, create)
if err != nil {
return nil, err
}
memo, err := s.composeMemo(memoRaw)
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
if err != nil {
return nil, err
}
@@ -57,13 +114,27 @@ func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
return memo, nil
}
func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
memoRaw, err := patchMemoRaw(s.db, patch)
func (s *Store) PatchMemo(ctx context.Context, patch *api.MemoPatch) (*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRaw, err := patchMemoRaw(ctx, tx, patch)
if err != nil {
return nil, err
}
memo, err := s.composeMemo(memoRaw)
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
if err != nil {
return nil, err
}
@@ -71,15 +142,21 @@ func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
return memo, nil
}
func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
memoRawList, err := findMemoRawList(s.db, find)
func (s *Store) FindMemoList(ctx context.Context, find *api.MemoFind) ([]*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRawList, err := findMemoRawList(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.Memo{}
for _, raw := range memoRawList {
memo, err := s.composeMemo(raw)
memo, err := s.ComposeMemo(ctx, raw.toMemo())
if err != nil {
return nil, err
}
@@ -90,8 +167,29 @@ func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
return list, nil
}
func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
list, err := findMemoRawList(s.db, find)
func (s *Store) FindMemo(ctx context.Context, find *api.MemoFind) (*api.Memo, error) {
if find.ID != nil {
memoRaw := &memoRaw{}
has, err := s.cache.FindCache(api.MemoCache, *find.ID, memoRaw)
if err != nil {
return nil, err
}
if has {
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
if err != nil {
return nil, err
}
return memo, nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findMemoRawList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -100,7 +198,12 @@ func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
memo, err := s.composeMemo(list[0])
memoRaw := list[0]
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
if err != nil {
return nil, err
}
@@ -108,44 +211,43 @@ func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
return memo, nil
}
func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
err := deleteMemo(s.db, delete)
func (s *Store) DeleteMemo(ctx context.Context, delete *api.MemoDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemo(ctx, tx, delete); err != nil {
return FormatError(err)
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.MemoCache, delete.ID)
return nil
}
func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content"}
placeholder := []string{"?", "?"}
args := []interface{}{create.CreatorID, create.Content}
if v := create.Visibility; v != nil {
set, placeholder, args = append(set, "visibility"), append(placeholder, "?"), append(args, *v)
}
if v := create.CreatedTs; v != nil {
set, placeholder, args = append(set, "created_ts"), append(placeholder, "?"), append(args, *v)
}
func createMemoRaw(ctx context.Context, tx *sql.Tx, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content", "visibility"}
args := []interface{}{create.CreatorID, create.Content, create.Visibility}
placeholder := []string{"?", "?", "?"}
query := `
INSERT INTO memo (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility`
row, err := db.Query(query,
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
INSERT INTO memo (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
`
var memoRaw memoRaw
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
@@ -160,36 +262,35 @@ func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
return &memoRaw, nil
}
func patchMemoRaw(db *sql.DB, patch *api.MemoPatch) (*memoRaw, error) {
func patchMemoRaw(ctx context.Context, tx *sql.Tx, patch *api.MemoPatch) (*memoRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.Content; v != nil {
set, args = append(set, "content = ?"), append(args, *v)
if v := patch.CreatedTs; v != nil {
set, args = append(set, "created_ts = ?"), append(args, *v)
}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Content; v != nil {
set, args = append(set, "content = ?"), append(args, *v)
}
if v := patch.Visibility; v != nil {
set, args = append(set, "visibility = ?"), append(args, *v)
}
args = append(args, patch.ID)
row, err := db.Query(`
query := `
UPDATE memo
SET `+strings.Join(set, ", ")+`
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
`
var memoRaw memoRaw
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
@@ -204,7 +305,7 @@ func patchMemoRaw(db *sql.DB, patch *api.MemoPatch) (*memoRaw, error) {
return &memoRaw, nil
}
func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*memoRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -217,24 +318,21 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Pinned; v != nil {
where = append(where, "id in (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id )")
where = append(where, "id IN (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)")
}
if v := find.ContentSearch; v != nil {
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
}
if v := find.Visibility; v != nil {
where, args = append(where, "visibility = ?"), append(args, *v)
}
pagination := ""
if find.Limit > 0 {
pagination = fmt.Sprintf("%s LIMIT %d", pagination, find.Limit)
if find.Offset > 0 {
pagination = fmt.Sprintf("%s OFFSET %d", pagination, find.Offset)
if v := find.VisibilityList; len(v) != 0 {
list := []string{}
for _, visibility := range v {
list = append(list, fmt.Sprintf("$%d", len(args)+1))
args = append(args, visibility)
}
where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ",")))
}
rows, err := db.Query(`
query := `
SELECT
id,
creator_id,
@@ -244,10 +342,10 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
content,
visibility
FROM memo
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`+pagination,
args...,
)
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY created_ts DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
@@ -278,32 +376,38 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
return memoRawList, nil
}
func deleteMemo(db *sql.DB, delete *api.MemoDelete) error {
result, err := db.Exec(`DELETE FROM memo WHERE id = ?`, delete.ID)
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
where, args := []string{"id = ?"}, []interface{}{delete.ID}
stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.ID)}
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo not found")}
}
return nil
}
func (s *Store) composeMemo(raw *memoRaw) (*api.Memo, error) {
memo := raw.toMemo()
memoOrganizer, err := s.FindMemoOrganizer(&api.MemoOrganizerFind{
MemoID: memo.ID,
UserID: memo.CreatorID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return nil, err
} else if memoOrganizer != nil {
memo.Pinned = memoOrganizer.Pinned
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return memo, nil
return nil
}
+100 -22
View File
@@ -1,8 +1,10 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
@@ -29,8 +31,14 @@ func (raw *memoOrganizerRaw) toMemoOrganizer() *api.MemoOrganizer {
}
}
func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
memoOrganizerRaw, err := findMemoOrganizer(s.db, find)
func (s *Store) FindMemoOrganizer(ctx context.Context, find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoOrganizerRaw, err := findMemoOrganizer(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -40,17 +48,44 @@ func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganiz
return memoOrganizer, nil
}
func (s *Store) UpsertMemoOrganizer(upsert *api.MemoOrganizerUpsert) error {
err := upsertMemoOrganizer(s.db, upsert)
func (s *Store) UpsertMemoOrganizer(ctx context.Context, upsert *api.MemoOrganizerUpsert) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := upsertMemoOrganizer(ctx, tx, upsert); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
row, err := db.Query(`
func (s *Store) DeleteMemoOrganizer(ctx context.Context, delete *api.MemoOrganizerDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemoOrganizer(ctx, tx, delete); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
query := `
SELECT
id,
memo_id,
@@ -58,7 +93,8 @@ func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerR
pinned
FROM memo_organizer
WHERE memo_id = ? AND user_id = ?
`, find.MemoID, find.UserID)
`
row, err := tx.QueryContext(ctx, query, find.MemoID, find.UserID)
if err != nil {
return nil, FormatError(err)
}
@@ -78,11 +114,15 @@ func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerR
return nil, FormatError(err)
}
if err := row.Err(); err != nil {
return nil, err
}
return &memoOrganizerRaw, nil
}
func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
row, err := db.Query(`
func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrganizerUpsert) error {
query := `
INSERT INTO memo_organizer (
memo_id,
user_id,
@@ -93,20 +133,9 @@ func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
SET
pinned = EXCLUDED.pinned
RETURNING id, memo_id, user_id, pinned
`,
upsert.MemoID,
upsert.UserID,
upsert.Pinned,
)
if err != nil {
return FormatError(err)
}
defer row.Close()
row.Next()
`
var memoOrganizer api.MemoOrganizer
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, upsert.MemoID, upsert.UserID, upsert.Pinned).Scan(
&memoOrganizer.ID,
&memoOrganizer.MemoID,
&memoOrganizer.UserID,
@@ -117,3 +146,52 @@ func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
return nil
}
func deleteMemoOrganizer(ctx context.Context, tx *sql.Tx, delete *api.MemoOrganizerDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_organizer WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo organizer not found")}
}
return nil
}
func vacuumMemoOrganizer(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_organizer
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}
+237
View File
@@ -0,0 +1,237 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
// memoResourceRaw is the store model for an MemoResource.
// Fields have exactly the same meanings as MemoResource.
type memoResourceRaw struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
func (raw *memoResourceRaw) toMemoResource() *api.MemoResource {
return &api.MemoResource{
MemoID: raw.MemoID,
ResourceID: raw.ResourceID,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
}
}
func (s *Store) FindMemoResourceList(ctx context.Context, find *api.MemoResourceFind) ([]*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoResourceRawList, err := findMemoResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.MemoResource{}
for _, raw := range memoResourceRawList {
memoResource := raw.toMemoResource()
list = append(list, memoResource)
}
return list, nil
}
func (s *Store) FindMemoResource(ctx context.Context, find *api.MemoResourceFind) (*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findMemoResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
memoResourceRaw := list[0]
return memoResourceRaw.toMemoResource(), nil
}
func (s *Store) UpsertMemoResource(ctx context.Context, upsert *api.MemoResourceUpsert) (*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoResourceRaw, err := upsertMemoResource(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
return memoResourceRaw.toMemoResource(), nil
}
func (s *Store) DeleteMemoResource(ctx context.Context, delete *api.MemoResourceDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemoResource(ctx, tx, delete); err != nil {
return FormatError(err)
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoResourceList(ctx context.Context, tx *sql.Tx, find *api.MemoResourceFind) ([]*memoResourceRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := find.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
query := `
SELECT
memo_id,
resource_id,
created_ts,
updated_ts
FROM memo_resource
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY updated_ts DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
memoResourceRawList := make([]*memoResourceRaw, 0)
for rows.Next() {
var memoResourceRaw memoResourceRaw
if err := rows.Scan(
&memoResourceRaw.MemoID,
&memoResourceRaw.ResourceID,
&memoResourceRaw.CreatedTs,
&memoResourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
memoResourceRawList = append(memoResourceRawList, &memoResourceRaw)
}
if err := rows.Err(); err != nil {
return nil, err
}
return memoResourceRawList, nil
}
func upsertMemoResource(ctx context.Context, tx *sql.Tx, upsert *api.MemoResourceUpsert) (*memoResourceRaw, error) {
set := []string{"memo_id", "resource_id"}
args := []interface{}{upsert.MemoID, upsert.ResourceID}
placeholder := []string{"?", "?"}
if v := upsert.UpdatedTs; v != nil {
set, args, placeholder = append(set, "updated_ts"), append(args, v), append(placeholder, "?")
}
query := `
INSERT INTO memo_resource (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
ON CONFLICT(memo_id, resource_id) DO UPDATE
SET
updated_ts = EXCLUDED.updated_ts
RETURNING memo_id, resource_id, created_ts, updated_ts
`
var memoResourceRaw memoResourceRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&memoResourceRaw.MemoID,
&memoResourceRaw.ResourceID,
&memoResourceRaw.CreatedTs,
&memoResourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &memoResourceRaw, nil
}
func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourceDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo resource not found")}
}
return nil
}
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_resource
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR resource_id NOT IN (
SELECT
id
FROM
resource
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}
+204 -35
View File
@@ -1,8 +1,10 @@
package store
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"github.com/usememos/memos/api"
@@ -43,9 +45,57 @@ func (raw *resourceRaw) toResource() *api.Resource {
}
}
func (s *Store) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
resourceRaw, err := createResource(s.db, create)
func (s *Store) ComposeMemoResourceList(ctx context.Context, memo *api.Memo) error {
resourceList, err := s.FindResourceList(ctx, &api.ResourceFind{
MemoID: &memo.ID,
})
if err != nil {
return err
}
for _, resource := range resourceList {
memoResource, err := s.FindMemoResource(ctx, &api.MemoResourceFind{
MemoID: &memo.ID,
ResourceID: &resource.ID,
})
if err != nil {
return err
}
resource.CreatedTs = memoResource.CreatedTs
resource.UpdatedTs = memoResource.UpdatedTs
}
sort.Slice(resourceList, func(i, j int) bool {
if resourceList[i].CreatedTs != resourceList[j].CreatedTs {
return resourceList[i].CreatedTs < resourceList[j].CreatedTs
}
return resourceList[i].ID < resourceList[j].ID
})
memo.ResourceList = resourceList
return nil
}
func (s *Store) CreateResource(ctx context.Context, create *api.ResourceCreate) (*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRaw, err := createResource(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
@@ -54,8 +104,14 @@ func (s *Store) CreateResource(create *api.ResourceCreate) (*api.Resource, error
return resource, nil
}
func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error) {
resourceRawList, err := findResourceList(s.db, find)
func (s *Store) FindResourceList(ctx context.Context, find *api.ResourceFind) ([]*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRawList, err := findResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -68,8 +124,25 @@ func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error
return resourceList, nil
}
func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
list, err := findResourceList(s.db, find)
func (s *Store) FindResource(ctx context.Context, find *api.ResourceFind) (*api.Resource, error) {
if find.ID != nil {
resourceRaw := &resourceRaw{}
has, err := s.cache.FindCache(api.ResourceCache, *find.ID, resourceRaw)
if err != nil {
return nil, err
}
if has {
return resourceRaw.toResource(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -78,22 +151,67 @@ func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
resource := list[0].toResource()
resourceRaw := list[0]
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
resource := resourceRaw.toResource()
return resource, nil
}
func (s *Store) DeleteResource(delete *api.ResourceDelete) error {
err := deleteResource(s.db, delete)
func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteResource(ctx, tx, delete); err != nil {
return err
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.ResourceCache, delete.ID)
return nil
}
func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error) {
row, err := db.Query(`
func (s *Store) PatchResource(ctx context.Context, patch *api.ResourcePatch) (*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRaw, err := patchResource(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
resource := resourceRaw.toResource()
return resource, nil
}
func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate) (*resourceRaw, error) {
query := `
INSERT INTO resource (
filename,
blob,
@@ -102,27 +220,16 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
creator_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, filename, blob, type, size, created_ts, updated_ts
`,
create.Filename,
create.Blob,
create.Type,
create.Size,
create.CreatorID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
`
var resourceRaw resourceRaw
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID).Scan(
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
@@ -132,7 +239,42 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
return &resourceRaw, nil
}
func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error) {
func patchResource(ctx context.Context, tx *sql.Tx, patch *api.ResourcePatch) (*resourceRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.Filename; v != nil {
set, args = append(set, "filename = ?"), append(args, *v)
}
args = append(args, patch.ID)
query := `
UPDATE resource
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
`
var resourceRaw resourceRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &resourceRaw, nil
}
func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) ([]*resourceRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -144,21 +286,25 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
if v := find.Filename; v != nil {
where, args = append(where, "filename = ?"), append(args, *v)
}
if v := find.MemoID; v != nil {
where, args = append(where, "id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
}
rows, err := db.Query(`
query := `
SELECT
id,
filename,
blob,
type,
size,
creator_id,
created_ts,
updated_ts
FROM resource
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY id DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
@@ -173,6 +319,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
@@ -189,15 +336,37 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
return resourceRawList, nil
}
func deleteResource(db *sql.DB, delete *api.ResourceDelete) error {
result, err := db.Exec(`DELETE FROM resource WHERE id = ?`, delete.ID)
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
where, args := []string{"id = ?"}, []interface{}{delete.ID}
stmt := `DELETE FROM resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.ID)}
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource not found")}
}
return nil
}
func vacuumResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
resource
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
+127 -44
View File
@@ -1,6 +1,7 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
@@ -39,9 +40,23 @@ func (raw *shortcutRaw) toShortcut() *api.Shortcut {
}
}
func (s *Store) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error) {
shortcutRaw, err := createShortcut(s.db, create)
func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate) (*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRaw, err := createShortcut(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
@@ -50,9 +65,23 @@ func (s *Store) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error
return shortcut, nil
}
func (s *Store) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
shortcutRaw, err := patchShortcut(s.db, patch)
func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRaw, err := patchShortcut(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
@@ -61,8 +90,14 @@ func (s *Store) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
return shortcut, nil
}
func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error) {
shortcutRawList, err := findShortcutList(s.db, find)
func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([]*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRawList, err := findShortcutList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -75,8 +110,25 @@ func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error
return list, nil
}
func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
list, err := findShortcutList(s.db, find)
func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.Shortcut, error) {
if find.ID != nil {
shortcutRaw := &shortcutRaw{}
has, err := s.cache.FindCache(api.ShortcutCache, *find.ID, shortcutRaw)
if err != nil {
return nil, err
}
if has {
return shortcutRaw.toShortcut(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findShortcutList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -85,22 +137,40 @@ func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
shortcut := list[0].toShortcut()
shortcutRaw := list[0]
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
shortcut := shortcutRaw.toShortcut()
return shortcut, nil
}
func (s *Store) DeleteShortcut(delete *api.ShortcutDelete) error {
err := deleteShortcut(s.db, delete)
func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
err = deleteShortcut(ctx, tx, delete)
if err != nil {
return FormatError(err)
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.ShortcutCache, *delete.ID)
return nil
}
func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error) {
row, err := db.Query(`
func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) (*shortcutRaw, error) {
query := `
INSERT INTO shortcut (
title,
payload,
@@ -108,19 +178,9 @@ func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error
)
VALUES (?, ?, ?)
RETURNING id, title, payload, creator_id, created_ts, updated_ts, row_status
`,
create.Title,
create.Payload,
create.CreatorID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
`
var shortcutRaw shortcutRaw
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, create.Title, create.Payload, create.CreatorID).Scan(
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
@@ -135,9 +195,12 @@ func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error
return &shortcutRaw, nil
}
func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.Title; v != nil {
set, args = append(set, "title = ?"), append(args, *v)
}
@@ -150,23 +213,14 @@ func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
args = append(args, patch.ID)
row, err := db.Query(`
query := `
UPDATE shortcut
SET `+strings.Join(set, ", ")+`
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, title, payload, created_ts, updated_ts, row_status
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
`
var shortcutRaw shortcutRaw
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
@@ -180,7 +234,7 @@ func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
return &shortcutRaw, nil
}
func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error) {
func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) ([]*shortcutRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -193,7 +247,7 @@ func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error
where, args = append(where, "title = ?"), append(args, *v)
}
rows, err := db.Query(`
rows, err := tx.QueryContext(ctx, `
SELECT
id,
title,
@@ -237,15 +291,44 @@ func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error
return shortcutRawList, nil
}
func deleteShortcut(db *sql.DB, delete *api.ShortcutDelete) error {
result, err := db.Exec(`DELETE FROM shortcut WHERE id = ?`, delete.ID)
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := delete.CreatorID; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
stmt := `DELETE FROM shortcut WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)}
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut not found")}
}
return nil
}
func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
shortcut
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
+56 -2
View File
@@ -1,21 +1,75 @@
package store
import (
"context"
"database/sql"
"github.com/usememos/memos/api"
"github.com/usememos/memos/server/profile"
)
// Store provides database access to all raw objects
// Store provides database access to all raw objects.
type Store struct {
db *sql.DB
profile *profile.Profile
cache api.CacheService
}
// New creates a new instance of Store
// New creates a new instance of Store.
func New(db *sql.DB, profile *profile.Profile) *Store {
cacheService := NewCacheService()
return &Store{
db: db,
profile: profile,
cache: cacheService,
}
}
func (s *Store) Vacuum(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
// Vacuum sqlite database file size after deleting resource.
if _, err := s.db.Exec("VACUUM"); err != nil {
return err
}
return nil
}
// Exec vacuum records in a transaction.
func vacuum(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemo(ctx, tx); err != nil {
return err
}
if err := vacuumResource(ctx, tx); err != nil {
return err
}
if err := vacuumShortcut(ctx, tx); err != nil {
return err
}
if err := vacuumUserSetting(ctx, tx); err != nil {
return err
}
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
return err
}
if err := vacuumMemoResource(ctx, tx); err != nil {
// Prevent revive warning.
return err
}
return nil
}
+154
View File
@@ -0,0 +1,154 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
type systemSettingRaw struct {
Name api.SystemSettingName
Value string
Description string
}
func (raw *systemSettingRaw) toSystemSetting() *api.SystemSetting {
return &api.SystemSetting{
Name: raw.Name,
Value: raw.Value,
Description: raw.Description,
}
}
func (s *Store) UpsertSystemSetting(ctx context.Context, upsert *api.SystemSettingUpsert) (*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRaw, err := upsertSystemSetting(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
systemSetting := systemSettingRaw.toSystemSetting()
return systemSetting, nil
}
func (s *Store) FindSystemSettingList(ctx context.Context, find *api.SystemSettingFind) ([]*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRawList, err := findSystemSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
list := []*api.SystemSetting{}
for _, raw := range systemSettingRawList {
list = append(list, raw.toSystemSetting())
}
return list, nil
}
func (s *Store) FindSystemSetting(ctx context.Context, find *api.SystemSettingFind) (*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRawList, err := findSystemSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(systemSettingRawList) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return systemSettingRawList[0].toSystemSetting(), nil
}
func upsertSystemSetting(ctx context.Context, tx *sql.Tx, upsert *api.SystemSettingUpsert) (*systemSettingRaw, error) {
query := `
INSERT INTO system_setting (
name, value, description
)
VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE
SET
value = EXCLUDED.value,
description = EXCLUDED.description
RETURNING name, value, description
`
var systemSettingRaw systemSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.Name, upsert.Value, upsert.Description).Scan(
&systemSettingRaw.Name,
&systemSettingRaw.Value,
&systemSettingRaw.Description,
); err != nil {
return nil, FormatError(err)
}
return &systemSettingRaw, nil
}
func findSystemSettingList(ctx context.Context, tx *sql.Tx, find *api.SystemSettingFind) ([]*systemSettingRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, v.String())
}
query := `
SELECT
name,
value,
description
FROM system_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
systemSettingRawList := make([]*systemSettingRaw, 0)
for rows.Next() {
var systemSettingRaw systemSettingRaw
if err := rows.Scan(
&systemSettingRaw.Name,
&systemSettingRaw.Value,
&systemSettingRaw.Description,
); err != nil {
return nil, FormatError(err)
}
systemSettingRawList = append(systemSettingRawList, &systemSettingRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return systemSettingRawList, nil
}
+190 -78
View File
@@ -1,6 +1,7 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
@@ -20,9 +21,10 @@ type userRaw struct {
UpdatedTs int64
// Domain specific fields
Email string
Username string
Role api.Role
Name string
Email string
Nickname string
PasswordHash string
OpenID string
}
@@ -35,17 +37,48 @@ func (raw *userRaw) toUser() *api.User {
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
Email: raw.Email,
Username: raw.Username,
Role: raw.Role,
Name: raw.Name,
Email: raw.Email,
Nickname: raw.Nickname,
PasswordHash: raw.PasswordHash,
OpenID: raw.OpenID,
}
}
func (s *Store) CreateUser(create *api.UserCreate) (*api.User, error) {
userRaw, err := createUser(s.db, create)
func (s *Store) ComposeMemoCreator(ctx context.Context, memo *api.Memo) error {
user, err := s.FindUser(ctx, &api.UserFind{
ID: &memo.CreatorID,
})
if err != nil {
return err
}
user.Email = ""
user.OpenID = ""
user.UserSettingList = nil
memo.Creator = user
return nil
}
func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRaw, err := createUser(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
@@ -54,9 +87,23 @@ func (s *Store) CreateUser(create *api.UserCreate) (*api.User, error) {
return user, nil
}
func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
userRaw, err := patchUser(s.db, patch)
func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRaw, err := patchUser(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
@@ -65,8 +112,14 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
return user, nil
}
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
userRawList, err := findUserList(s.db, find)
func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRawList, err := findUserList(ctx, tx, find)
if err != nil {
return nil, err
}
@@ -79,53 +132,94 @@ func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
return list, nil
}
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
list, err := findUserList(s.db, find)
func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, error) {
if find.ID != nil {
userRaw := &userRaw{}
has, err := s.cache.FindCache(api.UserCache, *find.ID, userRaw)
if err != nil {
return nil, err
}
if has {
return userRaw.toUser(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findUserList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
} else if len(list) > 1 {
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1. ", len(list), find)}
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)}
}
user := list[0].toUser()
userRaw := list[0]
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
user := userRaw.toUser()
return user, nil
}
func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
row, err := db.Query(`
func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteUser(ctx, tx, delete); err != nil {
return err
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
s.cache.DeleteCache(api.UserCache, delete.ID)
return nil
}
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
query := `
INSERT INTO user (
email,
username,
role,
name,
email,
nickname,
password_hash,
open_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
`,
create.Email,
VALUES (?, ?, ?, ?, ?, ?)
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query,
create.Username,
create.Role,
create.Name,
create.Email,
create.Nickname,
create.PasswordHash,
create.OpenID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var userRaw userRaw
if err := row.Scan(
).Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Username,
&userRaw.Role,
&userRaw.Name,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
@@ -138,17 +232,23 @@ func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
return &userRaw, nil
}
func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Username; v != nil {
set, args = append(set, "username = ?"), append(args, *v)
}
if v := patch.Email; v != nil {
set, args = append(set, "email = ?"), append(args, *v)
}
if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v)
if v := patch.Nickname; v != nil {
set, args = append(set, "nickname = ?"), append(args, *v)
}
if v := patch.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, *v)
@@ -159,74 +259,70 @@ func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
args = append(args, patch.ID)
row, err := db.Query(`
query := `
UPDATE user
SET `+strings.Join(set, ", ")+`
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
`, args...)
if err != nil {
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&userRaw.ID,
&userRaw.Username,
&userRaw.Role,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
defer row.Close()
if row.Next() {
var userRaw userRaw
if err := row.Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &userRaw, nil
}
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
return &userRaw, nil
}
func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = ?"), append(args, *v)
}
if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v)
}
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v)
}
if v := find.OpenID; v != nil {
where, args = append(where, "open_id = ?"), append(args, *v)
}
rows, err := db.Query(`
query := `
SELECT
id,
email,
username,
role,
name,
email,
nickname,
password_hash,
open_id,
created_ts,
updated_ts,
row_status
FROM user
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY created_ts DESC, row_status DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
@@ -237,16 +333,16 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
var userRaw userRaw
if err := rows.Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Username,
&userRaw.Role,
&userRaw.Name,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
fmt.Println(err)
return nil, FormatError(err)
}
@@ -259,3 +355,19 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
return userRawList, nil
}
func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
result, err := tx.ExecContext(ctx, `
DELETE FROM user WHERE id = ?
`, delete.ID)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user not found")}
}
return nil
}
+170
View File
@@ -0,0 +1,170 @@
package store
import (
"context"
"database/sql"
"strings"
"github.com/usememos/memos/api"
)
type userSettingRaw struct {
UserID int
Key api.UserSettingKey
Value string
}
func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
return &api.UserSetting{
UserID: raw.UserID,
Key: raw.Key,
Value: raw.Value,
}
}
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
userSetting := userSettingRaw.toUserSetting()
return userSetting, nil
}
func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRawList, err := findUserSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.UserSetting{}
for _, raw := range userSettingRawList {
list = append(list, raw.toUserSetting())
}
return list, nil
}
func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findUserSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
userSetting := list[0].toUserSetting()
return userSetting, nil
}
func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
query := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET
value = EXCLUDED.value
RETURNING user_id, key, value
`
var userSettingRaw userSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}
return &userSettingRaw, nil
}
func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Key; v != nil {
where, args = append(where, "key = ?"), append(args, v.String())
}
where, args = append(where, "user_id = ?"), append(args, find.UserID)
query := `
SELECT
user_id,
key,
value
FROM user_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
userSettingRawList := make([]*userSettingRaw, 0)
for rows.Next() {
var userSettingRaw userSettingRaw
if err := rows.Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}
userSettingRawList = append(userSettingRawList, &userSettingRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return userSettingRawList, nil
}
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
user_setting
WHERE
user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More