mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
Compare commits
673 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f61f2a0e | ||
|
|
c5368fe8d3 | ||
|
|
0aaf153717 | ||
|
|
942e1f887b | ||
|
|
b8ab43aa25 | ||
|
|
a5f3b051f2 | ||
|
|
4ba9767b94 | ||
|
|
12fda38520 | ||
|
|
9ed503fd6d | ||
|
|
a8976de634 | ||
|
|
f8855ddb56 | ||
|
|
288ecc617d | ||
|
|
14ec81b65c | ||
|
|
fae0b64a08 | ||
|
|
677750ef51 | ||
|
|
4cfd000b92 | ||
|
|
aacaf3f37c | ||
|
|
ad4a79a510 | ||
|
|
219d2754a0 | ||
|
|
10430a66c3 | ||
|
|
c167c21e4e | ||
|
|
40d25f7dca | ||
|
|
1441a1df1f | ||
|
|
b19c3c6db3 | ||
|
|
8c146aed68 | ||
|
|
805122f45c | ||
|
|
7d5de1a07e | ||
|
|
4b860777cf | ||
|
|
e29924c8a1 | ||
|
|
1847756ade | ||
|
|
771c56f485 | ||
|
|
0f057e81e9 | ||
|
|
529c9b34a7 | ||
|
|
e2e8130f4c | ||
|
|
46c13a4b7f | ||
|
|
96798e10b4 | ||
|
|
0f8ce3dd16 | ||
|
|
491859bbf6 | ||
|
|
f16123a624 | ||
|
|
d50ad9433f | ||
|
|
92a8a4ac0c | ||
|
|
62f53888ba | ||
|
|
79180928d4 | ||
|
|
e5550828a0 | ||
|
|
2e95f6824f | ||
|
|
5195012217 | ||
|
|
a797280e3f | ||
|
|
293f88e40c | ||
|
|
861eeb7b0f | ||
|
|
24b21aa9d7 | ||
|
|
51eac649c5 | ||
|
|
7670c95360 | ||
|
|
65e9fdead1 | ||
|
|
2b2792de73 | ||
|
|
c9bb2b785d | ||
|
|
64e5c343c5 | ||
|
|
9169b3f2cd | ||
|
|
b6f7a85a2a | ||
|
|
3556ae4e65 | ||
|
|
f888c62840 | ||
|
|
c160bed403 | ||
|
|
afc9709484 | ||
|
|
05b41804e3 | ||
|
|
2e2657b39d | ||
|
|
60ee602639 | ||
|
|
cac04e4406 | ||
|
|
278b4d21b4 | ||
|
|
27fd1e2880 | ||
|
|
fae9b3db46 | ||
|
|
49c7f49820 | ||
|
|
ef8981794e | ||
|
|
e52d77b2c4 | ||
|
|
1d2953b1b1 | ||
|
|
d702eaa625 | ||
|
|
50811c3064 | ||
|
|
99d9cc9168 | ||
|
|
119603da5d | ||
|
|
f6039f2eb9 | ||
|
|
65cc19c12e | ||
|
|
c07b4a57ca | ||
|
|
dca35bde87 | ||
|
|
9f25badde3 | ||
|
|
7efa749c66 | ||
|
|
72daa4e1d6 | ||
|
|
54702db9ba | ||
|
|
41ad084489 | ||
|
|
2fb171e069 | ||
|
|
201c0b020d | ||
|
|
b6f19ca093 | ||
|
|
68a77b6e1f | ||
|
|
e4a8a4d708 | ||
|
|
ab07c91d42 | ||
|
|
1838e616fd | ||
|
|
90d0ccc2e8 | ||
|
|
358a5c0ed9 | ||
|
|
40f39fd66c | ||
|
|
3b41976866 | ||
|
|
a23de50bb8 | ||
|
|
6596e6893e | ||
|
|
b6fe4d914e | ||
|
|
3c2cd43d28 | ||
|
|
2658b1fd09 | ||
|
|
b7df1f5bbf | ||
|
|
a0face6695 | ||
|
|
c177db69d5 | ||
|
|
b704c20809 | ||
|
|
6c17f94ef6 | ||
|
|
726285e634 | ||
|
|
bd6ab71d41 | ||
|
|
b67ed1ee13 | ||
|
|
55695f2189 | ||
|
|
e79d67d127 | ||
|
|
1d9ef9813a | ||
|
|
ef621a444f | ||
|
|
bd00fa798d | ||
|
|
a41745c9ae | ||
|
|
1eec474007 | ||
|
|
83e5278b51 | ||
|
|
a8751af6b5 | ||
|
|
6b24f52cd1 | ||
|
|
7ec22482c1 | ||
|
|
ee89dc00c0 | ||
|
|
bbd5fe4eb2 | ||
|
|
575a0610a3 | ||
|
|
b68cc08592 | ||
|
|
d51af7e98a | ||
|
|
334da5e903 | ||
|
|
35fed76d1a | ||
|
|
c77d49259a | ||
|
|
5520605ccc | ||
|
|
1dee8ae49f | ||
|
|
3fd4ee83ac | ||
|
|
5e978e2cfc | ||
|
|
37b7b983d2 | ||
|
|
c4278ef55a | ||
|
|
91220ea4a6 | ||
|
|
4bebbf3e1d | ||
|
|
5d8b8c37a5 | ||
|
|
564f20d13a | ||
|
|
c3adb1b152 | ||
|
|
688dc2304c | ||
|
|
dd6e2337e6 | ||
|
|
66418d4210 | ||
|
|
ab8c7b9d8a | ||
|
|
387799b31c | ||
|
|
4a64a4dea8 | ||
|
|
964c58ac01 | ||
|
|
56716cdad4 | ||
|
|
a2ee750d1e | ||
|
|
3f0601f651 | ||
|
|
6f8e3432e9 | ||
|
|
b7ab6f8e7e | ||
|
|
36b92ad884 | ||
|
|
4d9857ce18 | ||
|
|
43b22ce55f | ||
|
|
147185309c | ||
|
|
f48226d4f2 | ||
|
|
e92407d9ec | ||
|
|
79bf365d78 | ||
|
|
492a1370ab | ||
|
|
e3ddf93c4d | ||
|
|
4a9314c476 | ||
|
|
4767ee3293 | ||
|
|
1ea74dfd0d | ||
|
|
53cf6ebb79 | ||
|
|
d1007950e0 | ||
|
|
331226ec68 | ||
|
|
a7374cf998 | ||
|
|
e3d76193b9 | ||
|
|
07f0c3f052 | ||
|
|
a467a7c173 | ||
|
|
14f9f29348 | ||
|
|
5451fd2d2c | ||
|
|
f092771ea1 | ||
|
|
7c6d7226f5 | ||
|
|
eaebc6dcef | ||
|
|
c5200ca31b | ||
|
|
1078132b12 | ||
|
|
6b058cd299 | ||
|
|
b8f24af5ae | ||
|
|
6384f5af74 | ||
|
|
52038d26d2 | ||
|
|
55f37664ef | ||
|
|
ab8e3473a1 | ||
|
|
eba23c4f6e | ||
|
|
00fe6d3862 | ||
|
|
3646b8f5dd | ||
|
|
d40639bf8e | ||
|
|
12b81781b9 | ||
|
|
b67a37453d | ||
|
|
b04e001db1 | ||
|
|
fbe7b604ef | ||
|
|
0402cb7b27 | ||
|
|
b72bfc9c24 | ||
|
|
40e92f9463 | ||
|
|
f883dd9c1d | ||
|
|
d8bf55efb2 | ||
|
|
f982e83d0a | ||
|
|
3472a6db26 | ||
|
|
c79e51a91b | ||
|
|
ce795a2a7d | ||
|
|
045819c312 | ||
|
|
2fa01886da | ||
|
|
dd7d322c47 | ||
|
|
dfe71f33c2 | ||
|
|
db1d223448 | ||
|
|
54271c1598 | ||
|
|
1ee8ebc9e1 | ||
|
|
6e5537d131 | ||
|
|
90c85103c3 | ||
|
|
2d5d734da4 | ||
|
|
e1e5121dd7 | ||
|
|
85db6721de | ||
|
|
b511a7b634 | ||
|
|
88c3b1ad0f | ||
|
|
9fe15a03b3 | ||
|
|
500afffbcf | ||
|
|
5f3cade810 | ||
|
|
072851e3ba | ||
|
|
e2cbea2022 | ||
|
|
29880be283 | ||
|
|
f4e2b7319a | ||
|
|
ff0db82dc3 | ||
|
|
2daf085ce4 | ||
|
|
495f1f2041 | ||
|
|
89179f78c2 | ||
|
|
7ad7461a92 | ||
|
|
8cfcd201b0 | ||
|
|
abcd3cfafb | ||
|
|
50d41c456b | ||
|
|
1d41d53723 | ||
|
|
0dc003854f | ||
|
|
a4b7a77016 | ||
|
|
939e836d1b | ||
|
|
a0b35f7aa9 | ||
|
|
574e160a11 | ||
|
|
2042737004 | ||
|
|
a0667abec8 | ||
|
|
362306a9cb | ||
|
|
5e069f79a2 | ||
|
|
7c78fb064f | ||
|
|
11a7e897dc | ||
|
|
d8adc0afe9 | ||
|
|
17c1b97d23 | ||
|
|
5aee2f46ab | ||
|
|
013ded1e04 | ||
|
|
0d0f893b93 | ||
|
|
d2a87a4ddb | ||
|
|
b05a176490 | ||
|
|
d149926a39 | ||
|
|
2d49e96a8a | ||
|
|
9036bd478b | ||
|
|
477130aa85 | ||
|
|
878e0eabc8 | ||
|
|
62f63d4af7 | ||
|
|
1acf2f8b13 | ||
|
|
a4a5e539ed | ||
|
|
a2831b37c4 | ||
|
|
706b1b428f | ||
|
|
1690566413 | ||
|
|
a24b885236 | ||
|
|
c89a6665b6 | ||
|
|
797accbc2c | ||
|
|
66f9bc48bb | ||
|
|
de2eff474c | ||
|
|
67195859dc | ||
|
|
61abc6dd11 | ||
|
|
9fe9245727 | ||
|
|
906ec7994e | ||
|
|
5258b0a5b4 | ||
|
|
ceac53b6d0 | ||
|
|
3775d5c9c2 | ||
|
|
da80d4ef62 | ||
|
|
205ad0fd6d | ||
|
|
d208731f5f | ||
|
|
a90acdabb3 | ||
|
|
407d1cdcaa | ||
|
|
cb50bbc3cb | ||
|
|
2743268fd7 | ||
|
|
8cc0977a01 | ||
|
|
0809ec8c72 | ||
|
|
db93710f85 | ||
|
|
241c93c6b7 | ||
|
|
bf07ab9e2f | ||
|
|
fe05e6a464 | ||
|
|
d1aa6aa7f8 | ||
|
|
79af7e8abf | ||
|
|
a142d975d7 | ||
|
|
67691d1e99 | ||
|
|
9b827b4801 | ||
|
|
421f4dbf60 | ||
|
|
8f119c4265 | ||
|
|
9866702850 | ||
|
|
a313a9bb31 | ||
|
|
e53f5fdd29 | ||
|
|
b6c0a04394 | ||
|
|
1e3b8315a0 | ||
|
|
dc5d705f8c | ||
|
|
4f10c12092 | ||
|
|
3aa44f9f6d | ||
|
|
78b4733fb9 | ||
|
|
37bb3bc546 | ||
|
|
b43bfce254 | ||
|
|
0d6281ef6b | ||
|
|
8e2844e0c5 | ||
|
|
c27e38b11c | ||
|
|
cf75054106 | ||
|
|
4ed987229b | ||
|
|
a1068b6fe3 | ||
|
|
91a61e058a | ||
|
|
bebcabc292 | ||
|
|
5bdf15aece | ||
|
|
d347b7a91e | ||
|
|
2c653f170c | ||
|
|
75218ef826 | ||
|
|
b7fbbed895 | ||
|
|
f8c0d73d2d | ||
|
|
006cb56d28 | ||
|
|
55dee0df7e | ||
|
|
2a275b2875 | ||
|
|
4246232fbe | ||
|
|
9d8c9609c3 | ||
|
|
ef5492074e | ||
|
|
40d5686031 | ||
|
|
4276a7a56d | ||
|
|
d6fa1c7c80 | ||
|
|
6cbc3c78e2 | ||
|
|
29966451b6 | ||
|
|
64f0662a61 | ||
|
|
0ccfd0c743 | ||
|
|
0ea1733acc | ||
|
|
8fb59bbfb9 | ||
|
|
7c401040c8 | ||
|
|
43541bde2c | ||
|
|
1e01c4dc46 | ||
|
|
e85d368f87 | ||
|
|
95376f78f6 | ||
|
|
30daea0c4f | ||
|
|
b891e08928 | ||
|
|
94df09c8c0 | ||
|
|
bdf6d4d42a | ||
|
|
cb2e1ae355 | ||
|
|
9705406b82 | ||
|
|
4e00b1b0cd | ||
|
|
fe5ba6850b | ||
|
|
5690dab6bb | ||
|
|
65506a5b30 | ||
|
|
f09bb2689c | ||
|
|
e6f376ae66 | ||
|
|
1c2998c4d8 | ||
|
|
fc5d5cf231 | ||
|
|
2a4fc7dcc3 | ||
|
|
b68d6e2693 | ||
|
|
0b34b142c8 | ||
|
|
0b2a9d8511 | ||
|
|
7c9c5c316b | ||
|
|
180ae206c7 | ||
|
|
69e3ba6bed | ||
|
|
bf5b7e747d | ||
|
|
24154c95f2 | ||
|
|
0b10d24eb2 | ||
|
|
06ec59eca4 | ||
|
|
35a8817442 | ||
|
|
c7378e78d9 | ||
|
|
ab182dc22a | ||
|
|
f554e5a357 | ||
|
|
749486ba3c | ||
|
|
26aae0e637 | ||
|
|
95c8d8fc9c | ||
|
|
1021023577 | ||
|
|
3d0c6004c0 | ||
|
|
6c53bd00f6 | ||
|
|
cc759bef56 | ||
|
|
d670adc11f | ||
|
|
349c383604 | ||
|
|
f407488128 | ||
|
|
086b10717f | ||
|
|
eefd0444c8 | ||
|
|
65a61ed270 | ||
|
|
5e7db4631e | ||
|
|
0d6114e25e | ||
|
|
ce5a6fa3ac | ||
|
|
246851fdbe | ||
|
|
21c30ac157 | ||
|
|
269d92e637 | ||
|
|
ffe145d436 | ||
|
|
37cd841992 | ||
|
|
315ab94c94 | ||
|
|
1aa9963e07 | ||
|
|
88ade2c0b7 | ||
|
|
4ada7dce77 | ||
|
|
a323689ee6 | ||
|
|
2ea612e2fe | ||
|
|
ca2557eb7e | ||
|
|
e0f95686f3 | ||
|
|
29770e8bfb | ||
|
|
b959acc69d | ||
|
|
486cf8bdac | ||
|
|
ea911387f1 | ||
|
|
0b9b89db81 | ||
|
|
eaf89aa2f2 | ||
|
|
4bd373ba57 | ||
|
|
7b29c65f58 | ||
|
|
2298ac6ff3 | ||
|
|
4477f47c25 | ||
|
|
4a2fe3aaae | ||
|
|
0f65b8bdd3 | ||
|
|
778282ae29 | ||
|
|
85dc29bfb9 | ||
|
|
05e46ee4a8 | ||
|
|
6a3b052fa2 | ||
|
|
222c792539 | ||
|
|
51fb8ddb07 | ||
|
|
8e63b8f289 | ||
|
|
17c35be426 | ||
|
|
dfd461518f | ||
|
|
df5818cdc5 | ||
|
|
5894104524 | ||
|
|
09732df0f5 | ||
|
|
ae8f292306 | ||
|
|
9f8c0ce567 | ||
|
|
c7cf35c7de | ||
|
|
e5c9d8604d | ||
|
|
b2c22977c1 | ||
|
|
c0edb72b3d | ||
|
|
33d31b7dca | ||
|
|
4c465bef2d | ||
|
|
3bde9543f3 | ||
|
|
cff0e86989 | ||
|
|
65f7aa7914 | ||
|
|
06f5a5788e | ||
|
|
52c8ac2ad3 | ||
|
|
0c80654cc2 | ||
|
|
a2180f177f | ||
|
|
b992d07f3e | ||
|
|
dc3052e225 | ||
|
|
9b2e57cee5 | ||
|
|
77a3513a6b | ||
|
|
0dd2337663 | ||
|
|
d316c04837 | ||
|
|
63468dbaf3 | ||
|
|
2acd5d4af2 | ||
|
|
6c1cc1d283 | ||
|
|
15cfc9e1f5 | ||
|
|
004713d4cd | ||
|
|
7a6eb53e0f | ||
|
|
02c26d5bb4 | ||
|
|
c60c58ed69 | ||
|
|
366afdd1e4 | ||
|
|
307483e499 | ||
|
|
4608894e56 | ||
|
|
a1066322c8 | ||
|
|
050a2d39fa | ||
|
|
13aa61bbc0 | ||
|
|
f4d0e8c948 | ||
|
|
e7db587a9e | ||
|
|
059080f194 | ||
|
|
78c159cd59 | ||
|
|
721aa3c907 | ||
|
|
77178afad5 | ||
|
|
fd7b8c3293 | ||
|
|
660908e436 | ||
|
|
8d694f7732 | ||
|
|
bdfa9f7a56 | ||
|
|
19ef97a7ec | ||
|
|
2c17ea703d | ||
|
|
1591fdf61c | ||
|
|
811f3340e9 | ||
|
|
7e8d1128f8 | ||
|
|
54db6eda04 | ||
|
|
c5b26e3310 | ||
|
|
7079faf2b9 | ||
|
|
707d1a96eb | ||
|
|
76801dfa4f | ||
|
|
6e4577f721 | ||
|
|
7b0987610c | ||
|
|
50fa560d4a | ||
|
|
b8a7df21f2 | ||
|
|
d1a4348048 | ||
|
|
020b211660 | ||
|
|
4657e58b52 | ||
|
|
22971c3a93 | ||
|
|
5fa9aa3c22 | ||
|
|
fbce43870f | ||
|
|
b1e6956441 | ||
|
|
ad462cec29 | ||
|
|
a0a42285d0 | ||
|
|
b0b2776d03 | ||
|
|
e9ac6affef | ||
|
|
5eea1339c9 | ||
|
|
f303dc21a0 | ||
|
|
d68891d91d | ||
|
|
987bb80770 | ||
|
|
b884327a53 | ||
|
|
4743818fe7 | ||
|
|
43575e6f54 | ||
|
|
89f9dc5640 | ||
|
|
f7aca99d52 | ||
|
|
12a48ae2db | ||
|
|
bcb684d1cc | ||
|
|
d3b26f7126 | ||
|
|
422e190c96 | ||
|
|
3e13fa1ce6 | ||
|
|
dc9f531447 | ||
|
|
e330159f55 | ||
|
|
b88846fff5 | ||
|
|
93b6a910ae | ||
|
|
8d161b4526 | ||
|
|
b6acf62aab | ||
|
|
2a11aed881 | ||
|
|
e7b287902b | ||
|
|
c012ce0481 | ||
|
|
c97399a8ea | ||
|
|
75d622f4a2 | ||
|
|
5bdf7654fc | ||
|
|
62657f7f4e | ||
|
|
64332c3e6a | ||
|
|
57f51d1c58 | ||
|
|
9f3f730723 | ||
|
|
e9d303326f | ||
|
|
20d7112a05 | ||
|
|
922cc21abc | ||
|
|
cdbd934c4e | ||
|
|
ca1170583e | ||
|
|
f5629c8227 | ||
|
|
2f58032ad8 | ||
|
|
466bfe4b49 | ||
|
|
7d0407013e | ||
|
|
0e4e2e4bc5 | ||
|
|
a8a3cf31b4 | ||
|
|
f784516470 | ||
|
|
2aed7c70aa | ||
|
|
b1062f5387 | ||
|
|
29c835d27a | ||
|
|
0698c9c853 | ||
|
|
e54ff5ec9e | ||
|
|
f0a23f4620 | ||
|
|
c60bb12424 | ||
|
|
3b1bb4a95d | ||
|
|
05a5c59a7e | ||
|
|
734d5f3aed | ||
|
|
2cf07753e7 | ||
|
|
2935057ca7 | ||
|
|
68b30063a9 | ||
|
|
f06a3d171b | ||
|
|
a98e64cf0a | ||
|
|
dd04bc9e1d | ||
|
|
2f33eceada | ||
|
|
d5b88775d9 | ||
|
|
a7a01df79a | ||
|
|
e3fac742c5 | ||
|
|
ce390f3f79 | ||
|
|
43a8b7d0e1 | ||
|
|
b596d04939 | ||
|
|
84a3548232 | ||
|
|
63388a7d97 | ||
|
|
35f980d2b8 | ||
|
|
90b881502d | ||
|
|
dfac877957 | ||
|
|
87f5ac8b71 | ||
|
|
3c1a416afc | ||
|
|
972a49d6aa | ||
|
|
cea16fac88 | ||
|
|
7e994c8f11 | ||
|
|
646a41e931 | ||
|
|
735938395b | ||
|
|
d8e10ba399 | ||
|
|
8c28721839 | ||
|
|
da333b0b1e | ||
|
|
44d07ac401 | ||
|
|
d08ba56c28 | ||
|
|
c991a48df6 | ||
|
|
fd44255668 | ||
|
|
84564891be | ||
|
|
8c8bb9e59f | ||
|
|
99df4acfe9 | ||
|
|
47ffd99c3b | ||
|
|
c703f281d9 | ||
|
|
e179c65e52 | ||
|
|
29da70be56 | ||
|
|
a9dce26099 | ||
|
|
2c27f5d425 | ||
|
|
df7b4d54c6 | ||
|
|
9994b1fabc | ||
|
|
2d093d5be0 | ||
|
|
12b373701b | ||
|
|
2b8078a19b | ||
|
|
5617118fa8 | ||
|
|
fa93d0fd6e | ||
|
|
dc436490f8 | ||
|
|
d83f204d8c | ||
|
|
873973a088 | ||
|
|
d371cfd78d | ||
|
|
7b1bad5b29 | ||
|
|
0c2adfa1d2 | ||
|
|
07d9649b22 | ||
|
|
b7339e00ba | ||
|
|
58e68f8f80 | ||
|
|
cfa4151cff | ||
|
|
3d33b5d564 | ||
|
|
b516a8561f | ||
|
|
38383a426f | ||
|
|
7e34de23f1 | ||
|
|
f02ec375a6 | ||
|
|
3c5b0ea90a | ||
|
|
15e1037433 | ||
|
|
5da4c98f05 | ||
|
|
a73ee7aefc | ||
|
|
6c5bea9caf | ||
|
|
93ba2f4fab | ||
|
|
9417797b99 | ||
|
|
167e5596f2 | ||
|
|
2a1e34fe03 | ||
|
|
3de00cf4a8 | ||
|
|
1d55545e30 | ||
|
|
9b5a555d1f | ||
|
|
9c842d0a40 | ||
|
|
0dc377550f | ||
|
|
8a91b0ad9d | ||
|
|
1b50ab5dca | ||
|
|
6053df050c | ||
|
|
3517c6181d | ||
|
|
2e126c71f0 | ||
|
|
46d7ecca88 | ||
|
|
48d8c6ee0f | ||
|
|
5fd3cfdb61 | ||
|
|
10d710cf03 | ||
|
|
21702b615a | ||
|
|
d75338b6e9 | ||
|
|
b85af714f5 | ||
|
|
a2b32e0b75 | ||
|
|
0505598509 | ||
|
|
91d45d6d46 | ||
|
|
de7058532a | ||
|
|
7c94db0ca0 | ||
|
|
1d8603df2b | ||
|
|
6a8c559e8c | ||
|
|
7418d2965d | ||
|
|
ac560dfcf9 | ||
|
|
1afc183458 | ||
|
|
697d01e306 | ||
|
|
aed137472c | ||
|
|
bdc9632b5b | ||
|
|
6f32643d7c | ||
|
|
346d219cd5 | ||
|
|
6b5d5e757e | ||
|
|
e202d7b8d6 | ||
|
|
5a20db0bed | ||
|
|
2136a954f5 | ||
|
|
0e8d3e6907 | ||
|
|
592e037f21 | ||
|
|
c6695121f0 | ||
|
|
17a61bb65f | ||
|
|
49666ddaf3 | ||
|
|
29f73f0d25 | ||
|
|
3f3f6eaee8 | ||
|
|
f743532e57 | ||
|
|
58f62f88a8 | ||
|
|
3a837203a5 | ||
|
|
1a86b3cb5a | ||
|
|
d211f0f474 | ||
|
|
eb80bc7798 | ||
|
|
3b0346d84c | ||
|
|
65ade1fc87 | ||
|
|
5dd6d505cc | ||
|
|
2fe2b82809 | ||
|
|
06fc29aecd | ||
|
|
536627007d | ||
|
|
3c58953e56 | ||
|
|
0d317839d2 | ||
|
|
fa9443f121 | ||
|
|
a7425ac558 | ||
|
|
9611ff7386 |
@@ -1,2 +1 @@
|
||||
web/node_modules
|
||||
web/yarn.lock
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: usememos
|
||||
ko_fi: stevenlgtm
|
||||
31
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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.
|
||||
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -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.
|
||||
41
.github/workflows/backend-tests.yml
vendored
Normal file
41
.github/workflows/backend-tests.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Backend Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "release/*.*.*"
|
||||
|
||||
jobs:
|
||||
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"}'
|
||||
31
.github/workflows/build-and-push-dev-image.yml
vendored
31
.github/workflows/build-and-push-dev-image.yml
vendored
@@ -1,31 +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@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}/memos:dev
|
||||
@@ -4,35 +4,41 @@ on:
|
||||
push:
|
||||
branches:
|
||||
# Run on pushing branches like `release/1.0.0`
|
||||
- "release/v*.*.*"
|
||||
- "release/*.*.*"
|
||||
|
||||
jobs:
|
||||
build-and-push-release-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- 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@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}
|
||||
username: neosmemo
|
||||
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_NEOSMEMO_USERNAME }}/memos:${{ env.VERSION }}
|
||||
tags: neosmemo/memos:latest, neosmemo/memos:${{ env.VERSION }}
|
||||
|
||||
36
.github/workflows/build-and-push-test-image.yml
vendored
Normal file
36
.github/workflows/build-and-push-test-image.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: build-and-push-test-image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-and-push-test-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
|
||||
push: true
|
||||
tags: neosmemo/memos:test
|
||||
58
.github/workflows/codeql.yml
vendored
58
.github/workflows/codeql.yml
vendored
@@ -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
|
||||
|
||||
53
.github/workflows/frontend-tests.yml
vendored
Normal file
53
.github/workflows/frontend-tests.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Frontend Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "release/*.*.*"
|
||||
|
||||
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
|
||||
18
.github/workflows/issue-translator.yml
vendored
Normal file
18
.github/workflows/issue-translator.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: 'issue-translator'
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: false
|
||||
# not require, default false, . Decide whether to modify the issue title
|
||||
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
|
||||
CUSTOM_BOT_NOTE: Issue is not in English. It has been translated automatically.
|
||||
# not require. Customize the translation robot prefix message.
|
||||
97
.github/workflows/uffizzi-build.yml
vendored
Normal file
97
.github/workflows/uffizzi-build.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Build PR Image
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
jobs:
|
||||
build-memos:
|
||||
name: Build and push `Memos`
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
if: ${{ github.event.action != 'closed' }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Generate UUID image name
|
||||
id: uuid
|
||||
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
|
||||
tags: |
|
||||
type=raw,value=60d
|
||||
|
||||
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha, mode=max
|
||||
|
||||
render-compose-file:
|
||||
name: Render Docker Compose File
|
||||
# Pass output of this workflow to another triggered by `workflow_run` event.
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-memos
|
||||
outputs:
|
||||
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v3
|
||||
- name: Render Compose File
|
||||
run: |
|
||||
MEMOS_IMAGE=${{ needs.build-memos.outputs.tags }}
|
||||
export MEMOS_IMAGE
|
||||
# Render simple template from environment variables.
|
||||
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
|
||||
cat docker-compose.rendered.yml
|
||||
- name: Upload Rendered Compose File as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: docker-compose.rendered.yml
|
||||
retention-days: 2
|
||||
- name: Serialize PR Event to File
|
||||
run: |
|
||||
cat << EOF > event.json
|
||||
${{ toJSON(github.event) }}
|
||||
|
||||
EOF
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: event.json
|
||||
retention-days: 2
|
||||
|
||||
delete-preview:
|
||||
name: Call for Preview Deletion
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.action == 'closed' }}
|
||||
steps:
|
||||
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
|
||||
- name: Serialize PR Event to File
|
||||
run: |
|
||||
cat << EOF > event.json
|
||||
${{ toJSON(github.event) }}
|
||||
|
||||
EOF
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: event.json
|
||||
retention-days: 2
|
||||
86
.github/workflows/uffizzi-preview.yml
vendored
Normal file
86
.github/workflows/uffizzi-preview.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Deploy Uffizzi Preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Build PR Image"
|
||||
types:
|
||||
- completed
|
||||
|
||||
|
||||
jobs:
|
||||
cache-compose-file:
|
||||
name: Cache Compose File
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
compose-file-cache-key: ${{ env.HASH }}
|
||||
pr-number: ${{ env.PR_NUMBER }}
|
||||
steps:
|
||||
- name: 'Download artifacts'
|
||||
# Fetch output (zip archive) from the workflow run that triggered this workflow.
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "preview-spec"
|
||||
})[0];
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip preview-spec.zip
|
||||
- name: Read Event into ENV
|
||||
run: |
|
||||
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
|
||||
cat event.json >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
|
||||
- name: Hash Rendered Compose File
|
||||
id: hash
|
||||
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- name: Cache Rendered Compose File
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: docker-compose.rendered.yml
|
||||
key: ${{ env.HASH }}
|
||||
|
||||
- name: Read PR Number From Event Object
|
||||
id: pr
|
||||
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
|
||||
- name: DEBUG - Print Job Outputs
|
||||
if: ${{ runner.debug }}
|
||||
run: |
|
||||
echo "PR number: ${{ env.PR_NUMBER }}"
|
||||
echo "Compose file hash: ${{ env.HASH }}"
|
||||
cat event.json
|
||||
|
||||
deploy-uffizzi-preview:
|
||||
name: Use Remote Workflow to Preview on Uffizzi
|
||||
needs:
|
||||
- cache-compose-file
|
||||
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
|
||||
with:
|
||||
# If this workflow was triggered by a PR close event, cache-key will be an empty string
|
||||
# and this reusable workflow will delete the preview deployment.
|
||||
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
|
||||
compose-file-cache-path: docker-compose.rendered.yml
|
||||
server: https://app.uffizzi.com
|
||||
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,28 +1,21 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Temp output
|
||||
*.out
|
||||
*.log
|
||||
tmp
|
||||
|
||||
# Air (hot reload) generated
|
||||
.air
|
||||
|
||||
# Frontend asset
|
||||
dist
|
||||
# temp folder
|
||||
tmp
|
||||
|
||||
# Dev database
|
||||
data
|
||||
# Frontend asset
|
||||
web/dist
|
||||
|
||||
# build folder
|
||||
build
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
bin/air
|
||||
|
||||
64
.golangci.yaml
Normal file
64
.golangci.yaml
Normal 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
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.lintTool": "golangci-lint"
|
||||
}
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,32 +1,29 @@
|
||||
# Build frontend dist.
|
||||
FROM node:14.18.2-alpine3.14 AS frontend
|
||||
FROM node:18.12.1-alpine3.16 AS frontend
|
||||
WORKDIR /frontend-build
|
||||
|
||||
COPY ./web/ .
|
||||
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
RUN yarn && yarn build
|
||||
|
||||
# Build backend exec file.
|
||||
FROM golang:1.16.12-alpine3.15 AS backend
|
||||
FROM golang:1.19.3-alpine3.16 AS backend
|
||||
WORKDIR /backend-build
|
||||
|
||||
RUN apk --no-cache add gcc musl-dev
|
||||
RUN apk update && apk add --no-cache 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.14.3 AS monolithic
|
||||
FROM alpine:3.16 AS monolithic
|
||||
WORKDIR /usr/local/memos
|
||||
|
||||
COPY --from=backend /backend-build/memos /usr/local/memos/
|
||||
COPY --from=frontend /frontend-build/dist /usr/local/memos/web/dist
|
||||
|
||||
# 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"]
|
||||
|
||||
101
README.md
101
README.md
@@ -1,85 +1,84 @@
|
||||
<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" />
|
||||
<img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" />
|
||||
<img alt="Go report" src="https://goreportcard.com/badge/github.com/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>
|
||||
<a href="https://discord.gg/tfPJa4UmAv"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://memos.onrender.com/">Live Demo</a> •
|
||||
<a href="https://github.com/usememos/memos/discussions">Discussions</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>
|
||||
|
||||

|
||||

|
||||
|
||||
## 🎯 Intentions
|
||||
## Features
|
||||
|
||||
- ✍️ Write down the light-card memos very easily;
|
||||
- 🏗️ Build the fragmented knowledge management tool for yourself;
|
||||
- 📒 For noting your 📅 daily/weekly plans, 💡 fantastic ideas, 📕 reading thoughts...
|
||||
- 🦄 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.
|
||||
|
||||
## ✨ Features
|
||||
## Deploy with Docker in seconds
|
||||
|
||||
- 🦄 Fully open source;
|
||||
- 👍 Write in the plain textarea without any burden;
|
||||
- 🤠 Great UI and never miss any detail;
|
||||
- 🚀 Super quick self-hosted with `Docker` and `SQLite`;
|
||||
|
||||
## ⚓️ Deploy with Docker
|
||||
### Docker Run
|
||||
|
||||
```docker
|
||||
docker run --name memos --publish 5230:5230 --volume ~/.memos/:/var/opt/memos -e mode=prod -e port=5230 neosmemo/memos:0.1.2
|
||||
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.
|
||||
> The `~/.memos/` will be used as the data directory in your machine. And `/var/opt/memos` is the directory of the volume in docker and should not be modified.
|
||||
|
||||
## 🏗 Development
|
||||
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).
|
||||
|
||||
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
|
||||
### Docker Compose
|
||||
|
||||
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.
|
||||
Example Compose YAML file: [`docker-compose.yaml`](./docker-compose.yaml).
|
||||
|
||||
### Tech Stack
|
||||
If you want to upgrade the version of memos, use the following command.
|
||||
|
||||
<img alt="tech stack" src="https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png" width="360" />
|
||||
```sh
|
||||
docker-compose down && docker image rm neosmemo/memos:latest && docker-compose up -d
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
### Other guides
|
||||
|
||||
- [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)
|
||||
- [Deploy on render.com](./docs/deploy-with-render.md)
|
||||
- [Deploy on fly.io](https://github.com/hu3rror/memos-on-fly)
|
||||
|
||||
### Steps
|
||||
## Contribute
|
||||
|
||||
1. pull source code
|
||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
|
||||
|
||||
```bash
|
||||
git clone https://github.com/usememos/memos
|
||||
```
|
||||
See more in [development guide](./docs/development.md).
|
||||
|
||||
2. start backend using air(with live reload)
|
||||
## Products made by Community
|
||||
|
||||
```bash
|
||||
air -c scripts/.air.toml
|
||||
```
|
||||
- [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
|
||||
|
||||
3. start frontend dev server
|
||||
## User stories
|
||||
|
||||
```bash
|
||||
cd web && yarn && yarn dev
|
||||
```
|
||||
- [Memos - A Twitter Like Notes App You can Self Host](https://noted.lol/memos/)
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
### Join the community to build memos together!
|
||||
|
||||
## 🌟 Star history
|
||||
<a href="https://github.com/usememos/memos/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usememos/memos" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
This project is open source and available under the [MIT License](https://github.com/usememos/memos/blob/main/LICENSE).
|
||||
|
||||
## Star history
|
||||
|
||||
[](https://star-history.com/#usememos/memos&Date)
|
||||
|
||||
---
|
||||
|
||||
Just enjoy it.
|
||||
|
||||
7
SECURITY.md
Normal file
7
SECURITY.md
Normal 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).
|
||||
137
api/activity.go
Normal file
137
api/activity.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package api
|
||||
|
||||
import "github.com/usememos/memos/server/profile"
|
||||
|
||||
// ActivityType is the type for an activity.
|
||||
type ActivityType string
|
||||
|
||||
const (
|
||||
// User related.
|
||||
|
||||
// ActivityUserCreate is the type for creating users.
|
||||
ActivityUserCreate ActivityType = "user.create"
|
||||
// ActivityUserUpdate is the type for updating users.
|
||||
ActivityUserUpdate ActivityType = "user.update"
|
||||
// ActivityUserDelete is the type for deleting users.
|
||||
ActivityUserDelete ActivityType = "user.delete"
|
||||
// ActivityUserAuthSignIn is the type for user signin.
|
||||
ActivityUserAuthSignIn ActivityType = "user.auth.signin"
|
||||
// ActivityUserAuthSignUp is the type for user signup.
|
||||
ActivityUserAuthSignUp ActivityType = "user.auth.signup"
|
||||
// ActivityUserSettingUpdate is the type for updating user settings.
|
||||
ActivityUserSettingUpdate ActivityType = "user.setting.update"
|
||||
|
||||
// Memo related.
|
||||
|
||||
// ActivityMemoCreate is the type for creating memos.
|
||||
ActivityMemoCreate ActivityType = "memo.create"
|
||||
// ActivityMemoUpdate is the type for updating memos.
|
||||
ActivityMemoUpdate ActivityType = "memo.update"
|
||||
// ActivityMemoDelete is the type for deleting memos.
|
||||
ActivityMemoDelete ActivityType = "memo.delete"
|
||||
|
||||
// Shortcut related.
|
||||
|
||||
// ActivityShortcutCreate is the type for creating shortcuts.
|
||||
ActivityShortcutCreate ActivityType = "shortcut.create"
|
||||
// ActivityShortcutUpdate is the type for updating shortcuts.
|
||||
ActivityShortcutUpdate ActivityType = "shortcut.update"
|
||||
// ActivityShortcutDelete is the type for deleting shortcuts.
|
||||
ActivityShortcutDelete ActivityType = "shortcut.delete"
|
||||
|
||||
// Resource related.
|
||||
|
||||
// ActivityResourceCreate is the type for creating resources.
|
||||
ActivityResourceCreate ActivityType = "resource.create"
|
||||
// ActivityResourceDelete is the type for deleting resources.
|
||||
ActivityResourceDelete ActivityType = "resource.delete"
|
||||
|
||||
// Tag related.
|
||||
|
||||
// ActivityTagCreate is the type for creating tags.
|
||||
ActivityTagCreate ActivityType = "tag.create"
|
||||
// ActivityTagDelete is the type for deleting tags.
|
||||
ActivityTagDelete ActivityType = "tag.delete"
|
||||
|
||||
// Server related.
|
||||
|
||||
// ActivityServerStart is the type for starting server.
|
||||
ActivityServerStart ActivityType = "server.start"
|
||||
)
|
||||
|
||||
// ActivityLevel is the level of activities.
|
||||
type ActivityLevel string
|
||||
|
||||
const (
|
||||
// ActivityInfo is the INFO level of activities.
|
||||
ActivityInfo ActivityLevel = "INFO"
|
||||
// ActivityWarn is the WARN level of activities.
|
||||
ActivityWarn ActivityLevel = "WARN"
|
||||
// ActivityError is the ERROR level of activities.
|
||||
ActivityError ActivityLevel = "ERROR"
|
||||
)
|
||||
|
||||
type ActivityUserCreatePayload struct {
|
||||
UserID int `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
type ActivityUserAuthSignInPayload struct {
|
||||
UserID int `json:"userId"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type ActivityUserAuthSignUpPayload struct {
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type ActivityMemoCreatePayload struct {
|
||||
Content string `json:"content"`
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
|
||||
type ActivityShortcutCreatePayload struct {
|
||||
Title string `json:"title"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
type ActivityResourceCreatePayload struct {
|
||||
Filename string `json:"filename"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type ActivityTagCreatePayload struct {
|
||||
TagName string `json:"tagName"`
|
||||
}
|
||||
|
||||
type ActivityServerStartPayload struct {
|
||||
ServerID string `json:"serverId"`
|
||||
Profile *profile.Profile `json:"profile"`
|
||||
}
|
||||
|
||||
type Activity struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
// Standard fields
|
||||
CreatorID int `json:"creatorId"`
|
||||
CreatedTs int64 `json:"createdTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Type ActivityType `json:"type"`
|
||||
Level ActivityLevel `json:"level"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
// ActivityCreate is the API message for creating an activity.
|
||||
type ActivityCreate struct {
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
|
||||
// Domain specific fields
|
||||
Type ActivityType `json:"type"`
|
||||
Level ActivityLevel
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package api
|
||||
|
||||
// UnknownID is the ID for unknowns.
|
||||
const UnknownID = -1
|
||||
|
||||
// RowStatus is the status for a row.
|
||||
type RowStatus string
|
||||
|
||||
|
||||
11
api/auth.go
11
api/auth.go
@@ -1,13 +1,12 @@
|
||||
package api
|
||||
|
||||
type Login struct {
|
||||
Email string `json:"email"`
|
||||
type SignIn struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type Signup struct {
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
type SignUp struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
22
api/cache.go
Normal file
22
api/cache.go
Normal 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)
|
||||
}
|
||||
67
api/memo.go
67
api/memo.go
@@ -1,5 +1,29 @@
|
||||
package api
|
||||
|
||||
// Visibility is the type of a visibility.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// Public is the PUBLIC visibility.
|
||||
Public Visibility = "PUBLIC"
|
||||
// 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 Protected:
|
||||
return "PROTECTED"
|
||||
case Private:
|
||||
return "PRIVATE"
|
||||
}
|
||||
return "PRIVATE"
|
||||
}
|
||||
|
||||
type Memo struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
@@ -10,40 +34,55 @@ type Memo struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Content string `json:"content"`
|
||||
Pinned bool `json:"pinned"`
|
||||
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"`
|
||||
CreatorID int `json:"-"`
|
||||
|
||||
// Domain specific fields
|
||||
Content string `json:"content"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Content string `json:"content"`
|
||||
|
||||
// Related fields
|
||||
ResourceIDList []int `json:"resourceIdList"`
|
||||
}
|
||||
|
||||
type MemoPatch struct {
|
||||
ID int
|
||||
ID int `json:"-"`
|
||||
|
||||
// Standard fields
|
||||
CreatedTs *int64 `json:"createdTs"`
|
||||
UpdatedTs *int64
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
Content *string `json:"content"`
|
||||
Content *string `json:"content"`
|
||||
Visibility *Visibility `json:"visibility"`
|
||||
|
||||
// Related fields
|
||||
ResourceIDList []int `json:"resourceIdList"`
|
||||
}
|
||||
|
||||
type MemoFind struct {
|
||||
ID *int `json:"id"`
|
||||
ID *int
|
||||
|
||||
// Standard fields
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
CreatorID *int `json:"creatorId"`
|
||||
RowStatus *RowStatus
|
||||
CreatorID *int
|
||||
|
||||
// Domain specific fields
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
VisibilityList []Visibility
|
||||
|
||||
// Pagination
|
||||
Limit int
|
||||
@@ -51,5 +90,5 @@ type MemoFind struct {
|
||||
}
|
||||
|
||||
type MemoDelete struct {
|
||||
ID int `json:"id"`
|
||||
ID int
|
||||
}
|
||||
|
||||
@@ -9,13 +9,18 @@ type MemoOrganizer struct {
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
type MemoOrganizerUpsert struct {
|
||||
MemoID int `json:"-"`
|
||||
UserID int `json:"-"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type MemoOrganizerFind struct {
|
||||
MemoID int
|
||||
UserID int
|
||||
}
|
||||
|
||||
type MemoOrganizerUpsert struct {
|
||||
MemoID int
|
||||
UserID int
|
||||
Pinned bool `json:"pinned"`
|
||||
type MemoOrganizerDelete struct {
|
||||
MemoID *int
|
||||
UserID *int
|
||||
}
|
||||
|
||||
24
api/memo_resource.go
Normal file
24
api/memo_resource.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
type MemoResource struct {
|
||||
MemoID int
|
||||
ResourceID int
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
}
|
||||
|
||||
type MemoResourceUpsert struct {
|
||||
MemoID int `json:"-"`
|
||||
ResourceID int
|
||||
UpdatedTs *int64
|
||||
}
|
||||
|
||||
type MemoResourceFind struct {
|
||||
MemoID *int
|
||||
ResourceID *int
|
||||
}
|
||||
|
||||
type MemoResourceDelete struct {
|
||||
MemoID *int
|
||||
ResourceID *int
|
||||
}
|
||||
@@ -9,21 +9,26 @@ type Resource struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
Filename string `json:"filename"`
|
||||
Blob []byte `json:"blob"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
Filename string `json:"filename"`
|
||||
Blob []byte `json:"-"`
|
||||
ExternalLink string `json:"externalLink"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
|
||||
// Related fields
|
||||
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
||||
}
|
||||
|
||||
type ResourceCreate struct {
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
CreatorID int `json:"-"`
|
||||
|
||||
// Domain specific fields
|
||||
Filename string `json:"filename"`
|
||||
Blob []byte `json:"blob"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
Filename string `json:"filename"`
|
||||
Blob []byte `json:"-"`
|
||||
ExternalLink string `json:"externalLink"`
|
||||
Type string `json:"-"`
|
||||
Size int64 `json:"-"`
|
||||
}
|
||||
|
||||
type ResourceFind struct {
|
||||
@@ -32,6 +37,17 @@ type ResourceFind struct {
|
||||
// Standard fields
|
||||
CreatorID *int `json:"creatorId"`
|
||||
|
||||
// Domain specific fields
|
||||
Filename *string `json:"filename"`
|
||||
MemoID *int
|
||||
}
|
||||
|
||||
type ResourcePatch struct {
|
||||
ID int `json:"-"`
|
||||
|
||||
// Standard fields
|
||||
UpdatedTs *int64
|
||||
|
||||
// Domain specific fields
|
||||
Filename *string `json:"filename"`
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type Shortcut struct {
|
||||
|
||||
type ShortcutCreate struct {
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
CreatorID int `json:"-"`
|
||||
|
||||
// Domain specific fields
|
||||
Title string `json:"title"`
|
||||
@@ -24,9 +24,10 @@ type ShortcutCreate struct {
|
||||
}
|
||||
|
||||
type ShortcutPatch struct {
|
||||
ID int
|
||||
ID int `json:"-"`
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,17 @@ package api
|
||||
import "github.com/usememos/memos/server/profile"
|
||||
|
||||
type SystemStatus struct {
|
||||
Owner *User `json:"owner"`
|
||||
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"`
|
||||
// Customized server profile, including server name and external url.
|
||||
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
||||
}
|
||||
|
||||
139
api/system_setting.go
Normal file
139
api/system_setting.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type SystemSettingName string
|
||||
|
||||
const (
|
||||
// SystemSettingServerID is the key type of server id.
|
||||
SystemSettingServerID SystemSettingName = "serverId"
|
||||
// SystemSettingSecretSessionName is the key type of secret session name.
|
||||
SystemSettingSecretSessionName SystemSettingName = "secretSessionName"
|
||||
// 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"
|
||||
// SystemSettingCustomizedProfileName is the key type of customized server profile.
|
||||
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
|
||||
)
|
||||
|
||||
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
|
||||
type CustomizedProfile struct {
|
||||
// Name is the server name, default is `memos`
|
||||
Name string `json:"name"`
|
||||
// LogoURL is the url of logo image.
|
||||
LogoURL string `json:"logoUrl"`
|
||||
// Description is the server description.
|
||||
Description string `json:"description"`
|
||||
// Locale is the server default locale.
|
||||
Locale string `json:"locale"`
|
||||
// Appearance is the server default appearance.
|
||||
Appearance string `json:"appearance"`
|
||||
// ExternalURL is the external url of server. e.g. https://usermemos.com
|
||||
ExternalURL string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
func (key SystemSettingName) String() string {
|
||||
switch key {
|
||||
case SystemSettingServerID:
|
||||
return "serverId"
|
||||
case SystemSettingSecretSessionName:
|
||||
return "secretSessionName"
|
||||
case SystemSettingAllowSignUpName:
|
||||
return "allowSignUp"
|
||||
case SystemSettingAdditionalStyleName:
|
||||
return "additionalStyle"
|
||||
case SystemSettingAdditionalScriptName:
|
||||
return "additionalScript"
|
||||
case SystemSettingCustomizedProfileName:
|
||||
return "customizedProfile"
|
||||
}
|
||||
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 == SystemSettingServerID {
|
||||
return errors.New("update server id is not allowed")
|
||||
} else 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 if upsert.Name == SystemSettingCustomizedProfileName {
|
||||
customizedProfile := CustomizedProfile{
|
||||
Name: "memos",
|
||||
LogoURL: "",
|
||||
Description: "",
|
||||
Locale: "en",
|
||||
Appearance: "system",
|
||||
ExternalURL: "",
|
||||
}
|
||||
err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal system setting customized profile value")
|
||||
}
|
||||
if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) {
|
||||
return fmt.Errorf("invalid locale value")
|
||||
}
|
||||
if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
|
||||
return fmt.Errorf("invalid appearance value")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid system setting name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SystemSettingFind struct {
|
||||
Name *SystemSettingName `json:"name"`
|
||||
}
|
||||
20
api/tag.go
Normal file
20
api/tag.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package api
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
CreatorID int
|
||||
}
|
||||
|
||||
type TagUpsert struct {
|
||||
Name string
|
||||
CreatorID int `json:"-"`
|
||||
}
|
||||
|
||||
type TagFind struct {
|
||||
CreatorID int
|
||||
}
|
||||
|
||||
type TagDelete struct {
|
||||
Name string
|
||||
CreatorID int
|
||||
}
|
||||
104
api/user.go
104
api/user.go
@@ -1,19 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
)
|
||||
|
||||
// Role is the type of a role.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
// Owner is the OWNER role.
|
||||
Owner Role = "OWNER"
|
||||
// Host is the HOST role.
|
||||
Host Role = "HOST"
|
||||
// Admin is the ADMIN role.
|
||||
Admin Role = "ADMIN"
|
||||
// NormalUser is the USER role.
|
||||
NormalUser Role = "USER"
|
||||
)
|
||||
|
||||
func (e Role) String() string {
|
||||
switch e {
|
||||
case Owner:
|
||||
return "OWNER"
|
||||
case Host:
|
||||
return "HOST"
|
||||
case Admin:
|
||||
return "ADMIN"
|
||||
case NormalUser:
|
||||
return "USER"
|
||||
}
|
||||
@@ -29,38 +39,93 @@ 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.Username) > 32 {
|
||||
return fmt.Errorf("username is too long, maximum length is 32")
|
||||
}
|
||||
if len(create.Password) < 4 {
|
||||
return fmt.Errorf("password is too short, minimum length is 4")
|
||||
}
|
||||
if len(create.Nickname) > 64 {
|
||||
return fmt.Errorf("nickname is too long, maximum length is 64")
|
||||
}
|
||||
if create.Email != "" {
|
||||
if len(create.Email) > 256 {
|
||||
return fmt.Errorf("email is too long, maximum length is 256")
|
||||
}
|
||||
if !common.ValidateEmail(create.Email) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserPatch struct {
|
||||
ID int
|
||||
ID int `json:"-"`
|
||||
|
||||
// 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
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
func (patch UserPatch) Validate() error {
|
||||
if patch.Username != nil && len(*patch.Username) < 4 {
|
||||
return fmt.Errorf("username is too short, minimum length is 4")
|
||||
}
|
||||
if patch.Username != nil && len(*patch.Username) > 32 {
|
||||
return fmt.Errorf("username is too long, maximum length is 32")
|
||||
}
|
||||
if patch.Password != nil && len(*patch.Password) < 4 {
|
||||
return fmt.Errorf("password is too short, minimum length is 4")
|
||||
}
|
||||
if patch.Nickname != nil && len(*patch.Nickname) > 64 {
|
||||
return fmt.Errorf("nickname is too long, maximum length is 64")
|
||||
}
|
||||
if patch.Email != nil {
|
||||
if len(*patch.Email) > 256 {
|
||||
return fmt.Errorf("email is too long, maximum length is 256")
|
||||
}
|
||||
if !common.ValidateEmail(*patch.Email) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserFind struct {
|
||||
ID *int `json:"id"`
|
||||
|
||||
@@ -68,8 +133,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
|
||||
}
|
||||
|
||||
110
api/user_setting.go
Normal file
110
api/user_setting.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
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", "nl", "sv", "de", "es", "uk", "ru", "it"}
|
||||
UserSettingAppearanceValue = []string{"system", "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 `json:"-"`
|
||||
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")
|
||||
}
|
||||
if !slices.Contains(UserSettingLocaleValue, localeValue) {
|
||||
return fmt.Errorf("invalid user setting locale value")
|
||||
}
|
||||
} else if upsert.Key == UserSettingAppearanceKey {
|
||||
appearanceValue := "system"
|
||||
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal user setting appearance value")
|
||||
}
|
||||
if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
|
||||
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")
|
||||
}
|
||||
if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
|
||||
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")
|
||||
}
|
||||
if !slices.Contains(UserSettingMemoDisplayTsOptionKeyValue, memoDisplayTsOption) {
|
||||
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
|
||||
}
|
||||
@@ -1,59 +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
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
profile := profile.GetProfile()
|
||||
m := Main{
|
||||
profile: profile,
|
||||
}
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
|
||||
|
||||
if err := m.Run(); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,56 @@
|
||||
package main
|
||||
|
||||
import "github.com/usememos/memos/bin/server/cmd"
|
||||
import (
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
)
|
||||
|
||||
const (
|
||||
greetingBanner = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
`
|
||||
)
|
||||
|
||||
func run() error {
|
||||
ctx := context.Background()
|
||||
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("---")
|
||||
|
||||
serverInstance, err := server.NewServer(ctx, profile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start server")
|
||||
}
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
|
||||
return serverInstance.Run(ctx)
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
if err := run(); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
common/util_test.go
Normal file
31
common/util_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package common
|
||||
|
||||
// Version is the service current released version.
|
||||
var Version = "0.1.2"
|
||||
18
docker-compose.uffizzi.yml
Normal file
18
docker-compose.uffizzi.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: "3.0"
|
||||
|
||||
# uffizzi integration
|
||||
x-uffizzi:
|
||||
ingress:
|
||||
service: memos
|
||||
port: 5230
|
||||
|
||||
services:
|
||||
memos:
|
||||
image: "${MEMOS_IMAGE}"
|
||||
volumes:
|
||||
- memos_volume:/var/opt/memos
|
||||
command: ["--mode", "dev"]
|
||||
|
||||
volumes:
|
||||
memos_volume:
|
||||
|
||||
9
docker-compose.yaml
Normal file
9
docker-compose.yaml
Normal 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
|
||||
133
docs/deploy-with-render.md
Normal file
133
docs/deploy-with-render.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# A Beginner's Guide to Deploying Memos on Render.com
|
||||
|
||||
written by [AJ](https://memos.ajstephens.website/) (also a noob)
|
||||
|
||||
<img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" />
|
||||
|
||||
[Live Demo](https://demo.usememos.com) • [Official Website](https://usememos.com) • [Source Code](https://github.com/usememos/memos)
|
||||
|
||||
## Who is this guide for?
|
||||
|
||||
Someone who...
|
||||
|
||||
- doesn't have much experience with self hosting
|
||||
- has a minimal understanding of docker
|
||||
|
||||
Someone who wants...
|
||||
|
||||
- to use memos
|
||||
- to support the memos project
|
||||
- a cost effective and simple way to host it on the cloud with reliablity and persistance
|
||||
- to share memos with friends
|
||||
|
||||
## Requirements
|
||||
|
||||
- Can follow instructions
|
||||
- Have 7ish USD a month on a debit/credit card
|
||||
|
||||
## Guide
|
||||
|
||||
Create an account at [Render](https://dashboard.render.com/register)
|
||||

|
||||
|
||||
1. Go to your dashboard
|
||||
|
||||
[https://dashboard.render.com/](https://dashboard.render.com/)
|
||||
|
||||
2. Select New Web Service
|
||||
|
||||

|
||||
|
||||
3. Scroll down to "Public Git repository"
|
||||
|
||||
4. Paste in the link for the public git repository for memos (https://github.com/usememos/memos) and press continue
|
||||
|
||||

|
||||
|
||||
5. Render will pre-fill most of the fields but you will need to create a unique name for your web service
|
||||
|
||||
6. Adjust region if you want to
|
||||
|
||||
7. Don't touch the "branch", "root directory", and "environment" fields
|
||||
|
||||

|
||||
|
||||
8. Click "enter your payment information" and do so
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
9. Select the starter plan ($7 a month - a requirement for persistant data - render's free instances spin down when inactive and lose all data)
|
||||
|
||||
10. Click "Create Web Service"
|
||||
|
||||

|
||||
|
||||
11. Wait patiently while the _magic_ happens 🤷♂️
|
||||
|
||||

|
||||
|
||||
12. After some time (~ 6 min for me) the build will finish and you will see the web service is live
|
||||
|
||||

|
||||
|
||||
13. Now it's time to add the disk so your data won't dissappear when the webservice redeploys (redeploys happen automatically when the public repo is updated)
|
||||
|
||||
14. Select the "Disks" tab on the left menu and then click "Add Disk"
|
||||
|
||||

|
||||
|
||||
15. Name your disk (can be whatever)
|
||||
|
||||
16. Set the "Mount Path" to `/var/opt/memos`
|
||||
|
||||
17. Set the disk size (default is 10GB but 1GB is plenty and can be increased at any time)
|
||||
|
||||
18. Click "Save"
|
||||
|
||||

|
||||
|
||||
19. Wait...again...while the webservice redeploys with the persistant disk
|
||||
|
||||

|
||||
|
||||
20. aaaand....we're back online!
|
||||
|
||||

|
||||
|
||||
21. Time to test! We're going to make sure everything is working correctly.
|
||||
|
||||
22. Click the link in the top left, it should look like `https://the-name-you-chose.onrender.com` - this is your self hosted memos link!
|
||||
|
||||

|
||||
|
||||
23. Create a Username and Password (remember these) then click "Sign up as Host"
|
||||
|
||||

|
||||
|
||||
24. Create a test memo then click save
|
||||
|
||||

|
||||
|
||||
25. Sign out of your self-hosted memos
|
||||
|
||||

|
||||
|
||||
26. Return to your Render dashboard, click the "Manual Deploy" dropdown button and click "Deploy latest commit" and wait until the webservice is live again (This is to test that your data is persistant)
|
||||
|
||||

|
||||
|
||||
27. Once the webservice is live go back to your self-hosted memos page and sign in! (If your memos screen looks different then something went wrong)
|
||||
|
||||
28. Once you're logged in, verify your test memo is still there after the redeploy
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 🎉Celebrate!🎉
|
||||
|
||||
You did it! Enjoy using memos!
|
||||
|
||||
Want to learn more or need more guidance? Join the community on [telegram](https://t.me/+-_tNF1k70UU4ZTc9) and [discord](https://discord.gg/tfPJa4UmAv).
|
||||
40
docs/development.md
Normal file
40
docs/development.md
Normal 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
|
||||
|
||||

|
||||
|
||||
## 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:3001](http://localhost:3001) and change either frontend or backend code would trigger live reload.
|
||||
53
go.mod
53
go.mod
@@ -1,32 +1,53 @@
|
||||
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 (
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
)
|
||||
|
||||
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/sessions v1.2.1
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
)
|
||||
|
||||
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/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie 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
|
||||
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
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // 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.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
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/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo-contrib v0.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/segmentio/analytics-go v3.1.0+incompatible
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a
|
||||
golang.org/x/mod v0.6.0
|
||||
)
|
||||
|
||||
614
go.sum
614
go.sum
@@ -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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/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/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.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9IppkcT72GKnWjNf5W8=
|
||||
golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
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/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/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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=
|
||||
|
||||
98
plugin/http_getter/html_meta.go
Normal file
98
plugin/http_getter/html_meta.go
Normal 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
plugin/http_getter/html_meta_test.go
Normal file
28
plugin/http_getter/html_meta_test.go
Normal 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
plugin/http_getter/http_getter.go
Normal file
4
plugin/http_getter/http_getter.go
Normal 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
plugin/http_getter/image.go
Normal file
45
plugin/http_getter/image.go
Normal 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
plugin/http_getter/image_test.go
Normal file
21
plugin/http_getter/image_test.go
Normal 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
plugin/http_getter/util.go
Normal file
15
plugin/http_getter/util.go
Normal 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
|
||||
}
|
||||
14
plugin/metrics/collector.go
Normal file
14
plugin/metrics/collector.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package metric
|
||||
|
||||
// Metric is the API message for metric.
|
||||
type Metric struct {
|
||||
ID string
|
||||
Name string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// Collector is the interface definition for metric collector.
|
||||
type Collector interface {
|
||||
Identify(id string) error
|
||||
Collect(metric *Metric) error
|
||||
}
|
||||
45
plugin/metrics/segment/collector.go
Normal file
45
plugin/metrics/segment/collector.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package segment
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/segmentio/analytics-go"
|
||||
metric "github.com/usememos/memos/plugin/metrics"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// Identify will identify the server caller.
|
||||
func (c *collector) Identify(id string) error {
|
||||
return c.client.Enqueue(analytics.Identify{
|
||||
UserId: id,
|
||||
Timestamp: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
// 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{
|
||||
UserId: metric.ID,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Event: metric.Name,
|
||||
Properties: properties,
|
||||
})
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 562 KiB |
BIN
resources/demo.webp
Normal file
BIN
resources/demo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
BIN
resources/logo-full.webp
Normal file
BIN
resources/logo-full.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
resources/logo.webp
Normal file
BIN
resources/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -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
|
||||
|
||||
13
scripts/build.sh
Executable file
13
scripts/build.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./scripts/build.sh
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
echo "Start building backend..."
|
||||
|
||||
go build -o ./build/memos ./bin/server/main.go
|
||||
|
||||
echo "Backend built!"
|
||||
9
scripts/start.sh
Executable file
9
scripts/start.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./scripts/start.sh
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
air -c ./scripts/.air.toml
|
||||
95
server/acl.go
Normal file
95
server/acl.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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"
|
||||
sessionName = "memos_session"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
func setUserSession(ctx echo.Context, user *api.User) error {
|
||||
sess, _ := session.Get(sessionName, ctx)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 24 * 30,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
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(sessionName, 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()
|
||||
|
||||
if s.DefaultAuthSkipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
sess, _ := session.Get(sessionName, 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") && c.Request().Method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
userID := c.Get(getUserIDContextKey())
|
||||
if userID == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
174
server/auth.go
174
server/auth.go
@@ -5,41 +5,47 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
g.POST("/auth/login", func(c echo.Context) error {
|
||||
login := &api.Login{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(login); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted login request").SetInternal(err)
|
||||
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: &login.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", login.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", login.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", login.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.
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(login.Password)); err != nil {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
|
||||
// If the two passwords don't match, return a 401 status.
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect password").SetInternal(err)
|
||||
}
|
||||
|
||||
if err = setUserSession(c, user); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set login session").SetInternal(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
|
||||
}
|
||||
if err := s.createUserAuthSignInActivity(c, user); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
@@ -49,42 +55,53 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/auth/logout", func(c echo.Context) error {
|
||||
err := removeUserSession(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err)
|
||||
}
|
||||
|
||||
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 owner existed.
|
||||
ownerUserType := api.Owner
|
||||
ownerUserFind := api.UserFind{
|
||||
Role: &ownerUserType,
|
||||
}
|
||||
ownerUser, err := s.Store.FindUser(&ownerUserFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err)
|
||||
}
|
||||
if ownerUser != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Site Owner existed, please contact the site owner to signin account firstly.").SetInternal(err)
|
||||
}
|
||||
|
||||
signup := &api.Signup{}
|
||||
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 +109,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)
|
||||
}
|
||||
if err := s.createUserAuthSignUpActivity(c, user); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
err = setUserSession(c, user)
|
||||
if err != nil {
|
||||
@@ -115,4 +130,63 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/auth/signout", func(c echo.Context) error {
|
||||
err := removeUserSession(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set sign out session").SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createUserAuthSignInActivity(c echo.Context, user *api.User) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityUserAuthSignInPayload{
|
||||
UserID: user.ID,
|
||||
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: user.ID,
|
||||
Type: api.ActivityUserAuthSignIn,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) createUserAuthSignUpActivity(c echo.Context, user *api.User) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityUserAuthSignUpPayload{
|
||||
Username: user.Username,
|
||||
IP: echo.ExtractIPFromRealIPHeader()(c.Request()),
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: user.ID,
|
||||
Type: api.ActivityUserAuthSignUp,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,96 +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 {
|
||||
// Skips auth
|
||||
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,57 @@
|
||||
package server
|
||||
|
||||
func composeResponse(data interface{}) interface{} {
|
||||
type R struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
return R{
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func composeResponse(data interface{}) response {
|
||||
return response{
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultGetRequestSkipper(c echo.Context) bool {
|
||||
return c.Request().Method == http.MethodGet
|
||||
}
|
||||
|
||||
func DefaultAPIRequestSkipper(c echo.Context) bool {
|
||||
path := c.Path()
|
||||
return common.HasPrefixes(path, "/api")
|
||||
}
|
||||
|
||||
func (server *Server) DefaultAuthSkipper(c echo.Context) bool {
|
||||
ctx := c.Request().Context()
|
||||
path := c.Path()
|
||||
|
||||
// Skip auth.
|
||||
if common.HasPrefixes(path, "/api/auth") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 := server.Store.FindUser(ctx, userFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return false
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
13
server/dist/index.html
vendored
Normal file
13
server/dist/index.html
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- THIS FILE IS A PLACEHOLDER AND SHOULD NOT BE CHANGED -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Memos</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>No frontend embeded.</p>
|
||||
</body>
|
||||
</html>
|
||||
44
server/embed_frontend.go
Normal file
44
server/embed_frontend.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
//go:embed dist
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func getFileSystem(path string) http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return http.FS(fs)
|
||||
}
|
||||
|
||||
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{
|
||||
Skipper: DefaultAPIRequestSkipper,
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem("dist"),
|
||||
}))
|
||||
|
||||
assetsGroup := e.Group("assets")
|
||||
assetsGroup.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)
|
||||
}
|
||||
})
|
||||
assetsGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem("dist/assets"),
|
||||
}))
|
||||
}
|
||||
57
server/http_getter.go
Normal file
57
server/http_getter.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
getter "github.com/usememos/memos/plugin/http_getter"
|
||||
)
|
||||
|
||||
func registerGetterPublicRoutes(g *echo.Group) {
|
||||
g.GET("/get/httpmeta", func(c echo.Context) error {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
466
server/memo.go
466
server/memo.go
@@ -4,28 +4,77 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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)
|
||||
memoCreate := &api.MemoCreate{
|
||||
CreatorID: userID,
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
memoCreate := &api.MemoCreate{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
memoCreate.CreatorID = userID
|
||||
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
|
||||
}
|
||||
if err := s.createMemoCreateActivity(c, memo); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -35,23 +84,55 @@ 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)
|
||||
}
|
||||
|
||||
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
memoPatch := &api.MemoPatch{
|
||||
ID: memoID,
|
||||
ID: memoID,
|
||||
UpdatedTs: ¤tTs,
|
||||
}
|
||||
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)
|
||||
@@ -60,9 +141,24 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/memo", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
memoFind := &api.MemoFind{
|
||||
CreatorID: &userID,
|
||||
ctx := c.Request().Context()
|
||||
memoFind := &api.MemoFind{}
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
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 = ¤tUserID
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
}
|
||||
|
||||
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
|
||||
@@ -76,9 +172,17 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch := "#" + 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
|
||||
}
|
||||
@@ -86,39 +190,105 @@ 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/: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)
|
||||
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
|
||||
MemoID: memoID,
|
||||
UserID: userID,
|
||||
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{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
|
||||
}
|
||||
memoOrganizerUpsert.MemoID = memoID
|
||||
memoOrganizerUpsert.UserID = userID
|
||||
|
||||
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 {
|
||||
@@ -136,59 +306,81 @@ 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,
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
memo, err := s.Store.FindMemo(memoFind)
|
||||
memoResourceUpsert := &api.MemoResourceUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
|
||||
}
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &memoResourceUpsert.ResourceID,
|
||||
}
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
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, "Failed to fetch resource").SetInternal(err)
|
||||
}
|
||||
if resource == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
|
||||
} else if resource.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
|
||||
memoResourceUpsert.MemoID = memoID
|
||||
currentTs := time.Now().Unix()
|
||||
memoResourceUpsert.UpdatedTs = ¤tTs
|
||||
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo 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,
|
||||
}
|
||||
|
||||
err = s.Store.DeleteMemo(memoDelete)
|
||||
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
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.GET("/memo/amount", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
ctx := c.Request().Context()
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := &api.MemoFind{
|
||||
CreatorID: &userID,
|
||||
RowStatus: &normalRowStatus,
|
||||
}
|
||||
if userID, err := strconv.Atoi(c.QueryParam("userId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -199,4 +391,200 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
}
|
||||
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.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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
g.DELETE("/memo/:memoId/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")
|
||||
}
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
|
||||
ID: &memoID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||
}
|
||||
if memo.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createMemoCreateActivity(c echo.Context, memo *api.Memo) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityMemoCreatePayload{
|
||||
Content: memo.Content,
|
||||
Visibility: memo.Visibility.String(),
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: memo.CreatorID,
|
||||
Type: api.ActivityMemoCreate,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
62
server/metric_collector.go
Normal file
62
server/metric_collector.go
Normal file
@@ -0,0 +1,62 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// MetricCollector is the metric collector.
|
||||
type MetricCollector struct {
|
||||
collector metric.Collector
|
||||
ID string
|
||||
Enabled bool
|
||||
Profile *profile.Profile
|
||||
}
|
||||
|
||||
const (
|
||||
segmentMetricWriteKey = "NbPruMMmfqfD2AMCw3pkxZTsszVS3hKq"
|
||||
)
|
||||
|
||||
func (s *Server) registerMetricCollector() {
|
||||
c := segment.NewCollector(segmentMetricWriteKey)
|
||||
mc := &MetricCollector{
|
||||
collector: c,
|
||||
ID: s.ID,
|
||||
Enabled: false,
|
||||
Profile: s.Profile,
|
||||
}
|
||||
s.Collector = mc
|
||||
}
|
||||
|
||||
func (mc *MetricCollector) Identify(_ context.Context) {
|
||||
if !mc.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
err := mc.collector.Identify(mc.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to request segment, error: %+v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
|
||||
if !mc.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if metric.Labels == nil {
|
||||
metric.Labels = map[string]string{}
|
||||
}
|
||||
metric.Labels["mode"] = mc.Profile.Mode
|
||||
metric.Labels["version"] = version.GetCurrentVersion(mc.Profile.Mode)
|
||||
metric.ID = mc.ID
|
||||
err := mc.collector.Collect(metric)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to request segment, error: %+v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/version"
|
||||
)
|
||||
|
||||
// Profile is the configuration to start main server.
|
||||
@@ -16,6 +16,8 @@ type Profile struct {
|
||||
Mode string `json:"mode"`
|
||||
// Port is the binding port for server
|
||||
Port int `json:"port"`
|
||||
// Data is the data directory
|
||||
Data string `json:"data"`
|
||||
// DSN points to where Memos stores its own data
|
||||
DSN string `json:"dsn"`
|
||||
// Version is the current version of server
|
||||
@@ -36,42 +38,37 @@ func checkDSN(dataDir string) (string, error) {
|
||||
dataDir = strings.TrimRight(dataDir, "/")
|
||||
|
||||
if _, err := os.Stat(dataDir); err != nil {
|
||||
error := fmt.Errorf("unable to access -data %s, err %w", dataDir, err)
|
||||
return "", error
|
||||
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
|
||||
}
|
||||
|
||||
return dataDir, nil
|
||||
}
|
||||
|
||||
// GetDevProfile will return a profile for dev.
|
||||
func GetProfile() *Profile {
|
||||
mode := os.Getenv("mode")
|
||||
if mode != "dev" && mode != "prod" {
|
||||
mode = "dev"
|
||||
// GetDevProfile will return a profile for dev or prod.
|
||||
func GetProfile() (*Profile, error) {
|
||||
profile := Profile{}
|
||||
flag.StringVar(&profile.Mode, "mode", "dev", "mode of server")
|
||||
flag.IntVar(&profile.Port, "port", 8081, "port of server")
|
||||
flag.StringVar(&profile.Data, "data", "", "data directory")
|
||||
flag.Parse()
|
||||
|
||||
if profile.Mode != "dev" && profile.Mode != "prod" {
|
||||
profile.Mode = "dev"
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(os.Getenv("port"))
|
||||
if err != nil {
|
||||
port = 8080
|
||||
if profile.Mode == "prod" && profile.Data == "" {
|
||||
profile.Data = "/var/opt/memos"
|
||||
}
|
||||
|
||||
data := ""
|
||||
if mode == "prod" {
|
||||
data = "/var/opt/memos"
|
||||
}
|
||||
|
||||
dataDir, err := checkDSN(data)
|
||||
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
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s/memos_%s.db", dataDir, mode)
|
||||
profile.Data = dataDir
|
||||
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
|
||||
profile.Version = version.GetCurrentVersion(profile.Mode)
|
||||
|
||||
return &Profile{
|
||||
Mode: mode,
|
||||
Port: port,
|
||||
DSN: dsn,
|
||||
Version: common.Version,
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
@@ -1,28 +1,74 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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 << 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)
|
||||
resourceCreate := &api.ResourceCreate{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
|
||||
}
|
||||
|
||||
resourceCreate.CreatorID = userID
|
||||
resource, err := s.Store.CreateResource(ctx, resourceCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
|
||||
}
|
||||
if err := s.createResourceCreateActivity(c, resource); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").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
|
||||
})
|
||||
|
||||
g.POST("/resource/blob", 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")
|
||||
}
|
||||
|
||||
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,23 +81,25 @@ 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)
|
||||
}
|
||||
|
||||
resourceCreate := &api.ResourceCreate{
|
||||
CreatorID: userID,
|
||||
Filename: filename,
|
||||
Type: filetype,
|
||||
Size: size,
|
||||
Blob: fileBytes,
|
||||
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)
|
||||
}
|
||||
if err := s.createResourceCreateActivity(c, resource); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
|
||||
@@ -61,15 +109,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 +140,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,41 +167,171 @@ 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)
|
||||
}
|
||||
|
||||
c.Response().Writer.WriteHeader(http.StatusOK)
|
||||
c.Response().Writer.Header().Set("Content-Type", resource.Type)
|
||||
c.Response().Writer.Write(resource.Blob)
|
||||
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
|
||||
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 {
|
||||
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,
|
||||
}
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
|
||||
}
|
||||
if resource.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
resourcePatch := &api.ResourcePatch{
|
||||
UpdatedTs: ¤tTs,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
|
||||
}
|
||||
|
||||
resource.ID = resourceID
|
||||
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
|
||||
})
|
||||
|
||||
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.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
return nil
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
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, err := url.QueryUnescape(c.Param("filename"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("filename is invalid: %s", c.Param("filename"))).SetInternal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
resourceType := strings.ToLower(resource.Type)
|
||||
if strings.HasPrefix(resourceType, "text") || strings.HasPrefix(resourceType, "application") {
|
||||
resourceType = echo.MIMETextPlain
|
||||
}
|
||||
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
|
||||
if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
|
||||
http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(resource.Blob))
|
||||
return nil
|
||||
}
|
||||
return c.Stream(http.StatusOK, resourceType, bytes.NewReader(resource.Blob))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Resource) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityResourceCreatePayload{
|
||||
Filename: resource.Filename,
|
||||
Type: resource.Type,
|
||||
Size: resource.Size,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: resource.CreatorID,
|
||||
Type: api.ActivityResourceCreate,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
72
server/rss.go
Normal file
72
server/rss.go
Normal 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):]))
|
||||
})
|
||||
}
|
||||
142
server/server.go
142
server/server.go
@@ -1,13 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
metric "github.com/usememos/memos/plugin/metrics"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
"github.com/usememos/memos/store/db"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -17,53 +22,92 @@ import (
|
||||
type Server struct {
|
||||
e *echo.Echo
|
||||
|
||||
Profile *profile.Profile
|
||||
|
||||
Store *store.Store
|
||||
ID string
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
Collector *MetricCollector
|
||||
}
|
||||
|
||||
func NewServer(profile *profile.Profile) *Server {
|
||||
func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) {
|
||||
e := echo.New()
|
||||
e.Debug = true
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${method} ${uri} ${status}\n",
|
||||
}))
|
||||
|
||||
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
ErrorMessage: "Request timeout",
|
||||
Timeout: 30 * time.Second,
|
||||
}))
|
||||
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
Root: "web/dist",
|
||||
Browse: false,
|
||||
HTML5: true,
|
||||
}))
|
||||
|
||||
// In dev mode, set the const secret key to make login session persistence.
|
||||
secret := []byte("usememos")
|
||||
if profile.Mode == "prod" {
|
||||
secret = securecookie.GenerateRandomKey(16)
|
||||
}
|
||||
e.Use(session.Middleware(sessions.NewCookieStore(secret)))
|
||||
|
||||
s := &Server{
|
||||
e: e,
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
// Webhooks api skips auth checker.
|
||||
db := db.NewDB(profile)
|
||||
if err := db.Open(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot open db")
|
||||
}
|
||||
|
||||
storeInstance := store.New(db.DBInstance, profile)
|
||||
s.Store = storeInstance
|
||||
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: `{"time":"${time_rfc3339}",` +
|
||||
`"method":"${method}","uri":"${uri}",` +
|
||||
`"status":${status},"error":"${error}"}` + "\n",
|
||||
}))
|
||||
|
||||
e.Use(middleware.Gzip())
|
||||
|
||||
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
|
||||
Skipper: s.DefaultAuthSkipper,
|
||||
TokenLookup: "cookie:_csrf",
|
||||
}))
|
||||
|
||||
e.Use(middleware.CORS())
|
||||
|
||||
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
|
||||
Skipper: DefaultGetRequestSkipper,
|
||||
XSSProtection: "1; mode=block",
|
||||
ContentTypeNosniff: "nosniff",
|
||||
XFrameOptions: "SAMEORIGIN",
|
||||
HSTSPreloadEnabled: false,
|
||||
}))
|
||||
|
||||
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||
ErrorMessage: "Request timeout",
|
||||
Timeout: 30 * time.Second,
|
||||
}))
|
||||
|
||||
serverID, err := s.getSystemServerID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.ID = serverID
|
||||
|
||||
secretSessionName := "usememos"
|
||||
if profile.Mode == "prod" {
|
||||
secretSessionName, err = s.getSystemSecretSessionName(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
e.Use(session.Middleware(sessions.NewCookieStore([]byte(secretSessionName))))
|
||||
|
||||
embedFrontend(e)
|
||||
|
||||
// Register MetricCollector to server.
|
||||
s.registerMetricCollector()
|
||||
|
||||
rootGroup := e.Group("")
|
||||
s.registerRSSRoutes(rootGroup)
|
||||
|
||||
webhookGroup := e.Group("/h")
|
||||
s.registerWebhookRoutes(webhookGroup)
|
||||
s.registerResourcePublicRoutes(webhookGroup)
|
||||
|
||||
publicGroup := e.Group("/o")
|
||||
s.registerResourcePublicRoutes(publicGroup)
|
||||
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)
|
||||
@@ -73,9 +117,37 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
s.registerResourceRoutes(apiGroup)
|
||||
s.registerTagRoutes(apiGroup)
|
||||
|
||||
return s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (server *Server) Run() error {
|
||||
return server.e.Start(fmt.Sprintf(":%d", server.Profile.Port))
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
if err := s.createServerStartActivity(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Identify(ctx)
|
||||
return s.e.Start(fmt.Sprintf(":%d", s.Profile.Port))
|
||||
}
|
||||
|
||||
func (s *Server) createServerStartActivity(ctx context.Context) error {
|
||||
payload := api.ActivityServerStartPayload{
|
||||
ServerID: s.ID,
|
||||
Profile: s.Profile,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: api.UnknownID,
|
||||
Type: api.ActivityServerStart,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,26 +5,35 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"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)
|
||||
shortcutCreate := &api.ShortcutCreate{
|
||||
CreatorID: userID,
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
shortcutCreate := &api.ShortcutCreate{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(shortcutCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.CreateShortcut(shortcutCreate)
|
||||
shortcutCreate.CreatorID = userID
|
||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
|
||||
}
|
||||
if err := s.createShortcutCreateActivity(c, shortcut); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
|
||||
@@ -34,19 +43,37 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.PATCH("/shortcut/:shortcutId", 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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
shortcutFind := &api.ShortcutFind{
|
||||
ID: &shortcutID,
|
||||
}
|
||||
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
|
||||
}
|
||||
if shortcut.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
shortcutPatch := &api.ShortcutPatch{
|
||||
ID: shortcutID,
|
||||
UpdatedTs: ¤tTs,
|
||||
}
|
||||
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)
|
||||
shortcutPatch.ID = shortcutID
|
||||
shortcut, err = s.Store.PatchShortcut(ctx, shortcutPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
|
||||
}
|
||||
@@ -59,11 +86,16 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/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.StatusBadRequest, "Missing user id to find shortcut")
|
||||
}
|
||||
|
||||
shortcutFind := &api.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)
|
||||
}
|
||||
@@ -76,6 +108,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)
|
||||
@@ -84,7 +117,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)
|
||||
}
|
||||
@@ -97,19 +130,59 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.DELETE("/shortcut/:shortcutId", 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")
|
||||
}
|
||||
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,
|
||||
shortcutFind := &api.ShortcutFind{
|
||||
ID: &shortcutID,
|
||||
}
|
||||
if err := s.Store.DeleteShortcut(shortcutDelete); err != nil {
|
||||
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
|
||||
}
|
||||
if shortcut.CreatorID != userID {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
|
||||
shortcutDelete := &api.ShortcutDelete{
|
||||
ID: &shortcutID,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
return nil
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createShortcutCreateActivity(c echo.Context, shortcut *api.Shortcut) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityShortcutCreatePayload{
|
||||
Title: shortcut.Title,
|
||||
Payload: shortcut.Payload,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: shortcut.CreatorID,
|
||||
Type: api.ActivityShortcutCreate,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
214
server/system.go
214
server/system.go
@@ -1,10 +1,14 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -21,23 +25,100 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/status", func(c echo.Context) error {
|
||||
ownerUserType := api.Owner
|
||||
ownerUserFind := api.UserFind{
|
||||
Role: &ownerUserType,
|
||||
ctx := c.Request().Context()
|
||||
hostUserType := api.Host
|
||||
hostUserFind := api.UserFind{
|
||||
Role: &hostUserType,
|
||||
}
|
||||
ownerUser, err := s.Store.FindUser(&ownerUserFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err)
|
||||
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 ownerUser != nil {
|
||||
if hostUser != nil {
|
||||
// data desensitize
|
||||
ownerUser.OpenID = ""
|
||||
hostUser.OpenID = ""
|
||||
hostUser.Email = ""
|
||||
}
|
||||
|
||||
systemStatus := api.SystemStatus{
|
||||
Owner: ownerUser,
|
||||
Profile: s.Profile,
|
||||
Host: hostUser,
|
||||
Profile: *s.Profile,
|
||||
DBSize: 0,
|
||||
AllowSignUp: false,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
CustomizedProfile: api.CustomizedProfile{
|
||||
Name: "memos",
|
||||
LogoURL: "",
|
||||
Description: "",
|
||||
Locale: "en",
|
||||
Appearance: "system",
|
||||
ExternalURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
if systemSetting.Name == api.SystemSettingServerID || systemSetting.Name == api.SystemSettingSecretSessionName {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
|
||||
valueMap := value.(map[string]interface{})
|
||||
systemStatus.CustomizedProfile = api.CustomizedProfile{}
|
||||
if v := valueMap["name"]; v != nil {
|
||||
systemStatus.CustomizedProfile.Name = v.(string)
|
||||
}
|
||||
if v := valueMap["logoUrl"]; v != nil {
|
||||
systemStatus.CustomizedProfile.LogoURL = v.(string)
|
||||
}
|
||||
if v := valueMap["description"]; v != nil {
|
||||
systemStatus.CustomizedProfile.Description = v.(string)
|
||||
}
|
||||
if v := valueMap["locale"]; v != nil {
|
||||
systemStatus.CustomizedProfile.Locale = v.(string)
|
||||
}
|
||||
if v := valueMap["appearance"]; v != nil {
|
||||
systemStatus.CustomizedProfile.Appearance = v.(string)
|
||||
}
|
||||
if v := valueMap["externalUrl"]; v != nil {
|
||||
systemStatus.CustomizedProfile.ExternalURL = v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
// Get database size for host user.
|
||||
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 +127,117 @@ 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 || 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)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
g.POST("/system/vacuum", 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 || user.Role != api.Host {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||
}
|
||||
if err := s.Store.Vacuum(ctx); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
|
||||
}
|
||||
c.Response().WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getSystemServerID(ctx context.Context) (string, error) {
|
||||
serverIDKey := api.SystemSettingServerID
|
||||
serverIDValue, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||
Name: &serverIDKey,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return "", err
|
||||
}
|
||||
if serverIDValue == nil || serverIDValue.Value == "" {
|
||||
serverIDValue, err = s.Store.UpsertSystemSetting(ctx, &api.SystemSettingUpsert{
|
||||
Name: serverIDKey,
|
||||
Value: uuid.NewString(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return serverIDValue.Value, nil
|
||||
}
|
||||
|
||||
func (s *Server) getSystemSecretSessionName(ctx context.Context) (string, error) {
|
||||
secretSessionNameKey := api.SystemSettingSecretSessionName
|
||||
secretSessionNameValue, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||
Name: &secretSessionNameKey,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return "", err
|
||||
}
|
||||
if secretSessionNameValue == nil || secretSessionNameValue.Value == "" {
|
||||
secretSessionNameValue, err = s.Store.UpsertSystemSetting(ctx, &api.SystemSettingUpsert{
|
||||
Name: secretSessionNameKey,
|
||||
Value: uuid.NewString(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return secretSessionNameValue.Value, nil
|
||||
}
|
||||
|
||||
165
server/tag.go
165
server/tag.go
@@ -2,17 +2,85 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
g.POST("/tag", 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")
|
||||
}
|
||||
|
||||
tagUpsert := &api.TagUpsert{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(tagUpsert); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
|
||||
}
|
||||
if tagUpsert.Name == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
|
||||
}
|
||||
|
||||
tagUpsert.CreatorID = userID
|
||||
tag, err := s.Store.UpsertTag(ctx, tagUpsert)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert tag").SetInternal(err)
|
||||
}
|
||||
if err := s.createTagCreateActivity(c, tag); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tag.Name)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tag response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/tag", 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.StatusBadRequest, "Missing user id to find tag")
|
||||
}
|
||||
|
||||
tagFind := &api.TagFind{
|
||||
CreatorID: userID,
|
||||
}
|
||||
tagList, err := s.Store.FindTagList(ctx, tagFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
|
||||
}
|
||||
|
||||
tagNameList := []string{}
|
||||
for _, tag := range tagList {
|
||||
tagNameList = append(tagNameList, tag.Name)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagNameList)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/tag/suggestion", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user session")
|
||||
}
|
||||
contentSearch := "#"
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := api.MemoFind{
|
||||
@@ -21,28 +89,36 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
RowStatus: &normalRowStatus,
|
||||
}
|
||||
|
||||
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)
|
||||
tagFind := &api.TagFind{
|
||||
CreatorID: userID,
|
||||
}
|
||||
existTagList, err := s.Store.FindTagList(ctx, tagFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
|
||||
}
|
||||
tagNameList := []string{}
|
||||
for _, tag := range existTagList {
|
||||
tagNameList = append(tagNameList, tag.Name)
|
||||
}
|
||||
|
||||
tagMapSet := make(map[string]bool)
|
||||
for _, memo := range memoList {
|
||||
for _, rawTag := range r.FindAllString(memo.Content, -1) {
|
||||
tag := r.ReplaceAllString(rawTag, "$1")
|
||||
tagMapSet[tag] = true
|
||||
for _, tag := range findTagListFromMemoContent(memo.Content) {
|
||||
if !slices.Contains(tagNameList, tag) {
|
||||
tagMapSet[tag] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagList := []string{}
|
||||
for tag := range tagMapSet {
|
||||
tagList = append(tagList, tag)
|
||||
}
|
||||
sort.Strings(tagList)
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagList)); err != nil {
|
||||
@@ -50,4 +126,71 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.DELETE("/tag/:tagName", 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")
|
||||
}
|
||||
|
||||
tagName, err := url.QueryUnescape(c.Param("tagName"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid tag name").SetInternal(err)
|
||||
} else if tagName == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Tag name cannot be empty")
|
||||
}
|
||||
|
||||
tagDelete := &api.TagDelete{
|
||||
Name: tagName,
|
||||
CreatorID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteTag(ctx, tagDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Tag name not found: %s", tagName))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagName)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
var tagRegexp = regexp.MustCompile(`#([^\s#]+)`)
|
||||
|
||||
func findTagListFromMemoContent(memoContent string) []string {
|
||||
tagMapSet := make(map[string]bool)
|
||||
matches := tagRegexp.FindAllStringSubmatch(memoContent, -1)
|
||||
for _, v := range matches {
|
||||
tagName := v[1]
|
||||
tagMapSet[tagName] = true
|
||||
}
|
||||
|
||||
tagList := []string{}
|
||||
for tag := range tagMapSet {
|
||||
tagList = append(tagList, tag)
|
||||
}
|
||||
sort.Strings(tagList)
|
||||
return tagList
|
||||
}
|
||||
|
||||
func (s *Server) createTagCreateActivity(c echo.Context, tag *api.Tag) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityTagCreatePayload{
|
||||
TagName: tag.Name,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: tag.CreatorID,
|
||||
Type: api.ActivityTagCreate,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
47
server/tag_test.go
Normal file
47
server/tag_test.go
Normal 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", "tag2", "tag3", "tag4"},
|
||||
},
|
||||
{
|
||||
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
|
||||
want: []string{"tag1", "tag2", "tag2)", "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)
|
||||
}
|
||||
}
|
||||
}
|
||||
222
server/user.go
222
server/user.go
@@ -5,9 +5,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"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,10 +18,33 @@ import (
|
||||
|
||||
func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
g.POST("/user", 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")
|
||||
}
|
||||
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{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
|
||||
}
|
||||
if userCreate.Role == api.Host {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
|
||||
}
|
||||
userCreate.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(userCreate.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -26,10 +52,13 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if err := s.createUserCreateActivity(c, user); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||
@@ -39,11 +68,18 @@ 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 = ""
|
||||
user.Email = ""
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -53,20 +89,83 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
|
||||
// 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 {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
|
||||
userID := userSessionID.(int)
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
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(ctx, &api.UserFind{
|
||||
ID: &id,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
// data desensitize
|
||||
user.OpenID = ""
|
||||
user.Email = ""
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -74,14 +173,39 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.PATCH("/user/me", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
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.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: ¤tUserID,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
currentTs := time.Now().Unix()
|
||||
userPatch := &api.UserPatch{
|
||||
ID: userID,
|
||||
UpdatedTs: ¤tTs,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
|
||||
}
|
||||
userPatch.ID = userID
|
||||
if err := userPatch.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user patch format").SetInternal(err)
|
||||
}
|
||||
|
||||
if userPatch.Password != nil && *userPatch.Password != "" {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
|
||||
@@ -98,11 +222,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)
|
||||
@@ -110,9 +242,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: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -120,41 +256,51 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
}
|
||||
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.Owner {
|
||||
} else if currentUser.Role != api.Host {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) createUserCreateActivity(c echo.Context, user *api.User) error {
|
||||
ctx := c.Request().Context()
|
||||
payload := api.ActivityUserCreatePayload{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal activity payload")
|
||||
}
|
||||
activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
|
||||
CreatorID: user.ID,
|
||||
Type: api.ActivityUserCreate,
|
||||
Level: api.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
})
|
||||
if err != nil || activity == nil {
|
||||
return errors.Wrap(err, "failed to create activity")
|
||||
}
|
||||
s.Collector.Collect(ctx, &metric.Metric{
|
||||
Name: string(activity.Type),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
61
server/version/version.go
Normal file
61
server/version/version.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Version is the service current released version.
|
||||
// Semantic versioning: https://semver.org/
|
||||
var Version = "0.10.2"
|
||||
|
||||
// DevVersion is the service current development version.
|
||||
var DevVersion = "0.10.2"
|
||||
|
||||
func GetCurrentVersion(mode string) string {
|
||||
if mode == "dev" {
|
||||
return DevVersion
|
||||
}
|
||||
return Version
|
||||
}
|
||||
|
||||
func GetMinorVersion(version string) string {
|
||||
versionList := strings.Split(version, ".")
|
||||
if len(versionList) < 3 {
|
||||
return ""
|
||||
}
|
||||
return versionList[0] + "." + versionList[1]
|
||||
}
|
||||
|
||||
func GetSchemaVersion(version string) string {
|
||||
minorVersion := GetMinorVersion(version)
|
||||
return minorVersion + ".0"
|
||||
}
|
||||
|
||||
// IsVersionGreaterThanOrEqualTo returns true if version is greater than or equal to target.
|
||||
func IsVersionGreaterOrEqualThan(version, target string) bool {
|
||||
return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > -1
|
||||
}
|
||||
|
||||
// IsVersionGreaterThan returns true if version is greater than target.
|
||||
func IsVersionGreaterThan(version, target string) bool {
|
||||
return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > 0
|
||||
}
|
||||
|
||||
type SortVersion []string
|
||||
|
||||
func (s SortVersion) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s SortVersion) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s SortVersion) Less(i, j int) bool {
|
||||
v1 := fmt.Sprintf("v%s", s[i])
|
||||
v2 := fmt.Sprintf("v%s", s[j])
|
||||
return semver.Compare(v1, v2) == -1
|
||||
}
|
||||
93
server/version/version_test.go
Normal file
93
server/version/version_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsVersionGreaterOrEqualThan(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
target string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
version: "0.9.1",
|
||||
target: "0.9.1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
version: "0.10.0",
|
||||
target: "0.9.1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
version: "0.9.0",
|
||||
target: "0.9.1",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
result := IsVersionGreaterOrEqualThan(test.version, test.target)
|
||||
if result != test.want {
|
||||
t.Errorf("got result %v, want %v.", result, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVersionGreaterThan(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
target string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
version: "0.9.1",
|
||||
target: "0.9.1",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
version: "0.10.0",
|
||||
target: "0.8.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
version: "0.8.0",
|
||||
target: "0.10.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
version: "0.9.0",
|
||||
target: "0.9.1",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
result := IsVersionGreaterThan(test.version, test.target)
|
||||
if result != test.want {
|
||||
t.Errorf("got result %v, want %v.", result, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
versionList []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
versionList: []string{"0.9.1", "0.10.0", "0.8.0"},
|
||||
want: []string{"0.8.0", "0.9.1", "0.10.0"},
|
||||
},
|
||||
{
|
||||
versionList: []string{"1.9.1", "0.9.1", "0.10.0", "0.8.0"},
|
||||
want: []string{"0.8.0", "0.9.1", "0.10.0", "1.9.1"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
sort.Sort(SortVersion(test.versionList))
|
||||
assert.Equal(t, test.versionList, test.want)
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"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.POST("/:openId/memo", func(c echo.Context) error {
|
||||
openID := c.Param("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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User openId not found: %s", openID))
|
||||
}
|
||||
|
||||
memoCreate := &api.MemoCreate{
|
||||
CreatorID: user.ID,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request by open api").SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.CreateMemo(memoCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create 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)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.PATCH("/:openId/memo/:memoId", func(c echo.Context) error {
|
||||
openID := c.Param("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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User openId not found: %s", openID))
|
||||
}
|
||||
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("memoId is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
memoPatch := &api.MemoPatch{
|
||||
ID: memoID,
|
||||
}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request by open api").SetInternal(err)
|
||||
}
|
||||
|
||||
memo, err := s.Store.PatchMemo(memoPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch 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)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/:openId/memo", func(c echo.Context) error {
|
||||
openID := c.Param("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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Not found user with openid: %s", openID))
|
||||
}
|
||||
|
||||
memoFind := &api.MemoFind{
|
||||
CreatorID: &user.ID,
|
||||
}
|
||||
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
|
||||
if rowStatus != "" {
|
||||
memoFind.RowStatus = &rowStatus
|
||||
}
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
memoFind.Pinned = &pinned
|
||||
}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch := tag + " "
|
||||
memoFind.ContentSearch = &contentSearch
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
list, err := s.Store.FindMemoList(memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
||||
}
|
||||
|
||||
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 memo list response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/:openId/resource", func(c echo.Context) error {
|
||||
openID := c.Param("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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User openId not found: %s", openID))
|
||||
}
|
||||
|
||||
if err := c.Request().ParseMultipartForm(64 << 20); 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.StatusBadRequest, "Upload file not found").SetInternal(err)
|
||||
}
|
||||
|
||||
filename := file.Filename
|
||||
filetype := file.Header.Get("Content-Type")
|
||||
size := file.Size
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(src)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
||||
}
|
||||
|
||||
resourceCreate := &api.ResourceCreate{
|
||||
Filename: filename,
|
||||
Type: filetype,
|
||||
Size: size,
|
||||
Blob: fileBytes,
|
||||
CreatorID: user.ID,
|
||||
}
|
||||
|
||||
resource, err := s.Store.CreateResource(resourceCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create 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
|
||||
})
|
||||
|
||||
g.GET("/:openId/tag", func(c echo.Context) error {
|
||||
openID := c.Param("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 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User openId not found: %s", openID))
|
||||
}
|
||||
|
||||
contentSearch := "#"
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := api.MemoFind{
|
||||
CreatorID: &user.ID,
|
||||
ContentSearch: &contentSearch,
|
||||
RowStatus: &normalRowStatus,
|
||||
}
|
||||
|
||||
memoList, err := s.Store.FindMemoList(&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")
|
||||
tagMapSet[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
tagList := []string{}
|
||||
for tag := range tagMapSet {
|
||||
tagList = append(tagList, tag)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(tagList)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode tags response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
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)
|
||||
c.Response().Writer.Write(resource.Blob)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
89
store/activity.go
Normal file
89
store/activity.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
)
|
||||
|
||||
// activityRaw is the store model for an Activity.
|
||||
// Fields have exactly the same meanings as Activity.
|
||||
type activityRaw struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
CreatedTs int64
|
||||
|
||||
// Domain specific fields
|
||||
Type api.ActivityType
|
||||
Level api.ActivityLevel
|
||||
Payload string
|
||||
}
|
||||
|
||||
// toActivity creates an instance of Activity based on the ActivityRaw.
|
||||
func (raw *activityRaw) toActivity() *api.Activity {
|
||||
return &api.Activity{
|
||||
ID: raw.ID,
|
||||
|
||||
CreatorID: raw.CreatorID,
|
||||
CreatedTs: raw.CreatedTs,
|
||||
|
||||
Type: raw.Type,
|
||||
Level: raw.Level,
|
||||
Payload: raw.Payload,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateActivity creates an instance of Activity.
|
||||
func (s *Store) CreateActivity(ctx context.Context, create *api.ActivityCreate) (*api.Activity, error) {
|
||||
if s.profile.Mode != "dev" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
activityRaw, err := createActivity(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
activity := activityRaw.toActivity()
|
||||
return activity, nil
|
||||
}
|
||||
|
||||
// createActivity creates a new activity.
|
||||
func createActivity(ctx context.Context, tx *sql.Tx, create *api.ActivityCreate) (*activityRaw, error) {
|
||||
query := `
|
||||
INSERT INTO activity (
|
||||
creator_id,
|
||||
type,
|
||||
level,
|
||||
payload
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id, type, level, payload, creator_id, created_ts
|
||||
`
|
||||
var activityRaw activityRaw
|
||||
if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.Type, create.Level, create.Payload).Scan(
|
||||
&activityRaw.ID,
|
||||
&activityRaw.Type,
|
||||
&activityRaw.Level,
|
||||
&activityRaw.Payload,
|
||||
&activityRaw.CreatedTs,
|
||||
&activityRaw.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &activityRaw, nil
|
||||
}
|
||||
72
store/cache.go
Normal file
72
store/cache.go
Normal 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...))
|
||||
}
|
||||
}
|
||||
240
store/db/db.go
240
store/db/db.go
@@ -1,18 +1,19 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"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
|
||||
@@ -23,143 +24,220 @@ var seedFS embed.FS
|
||||
|
||||
type DB struct {
|
||||
// sqlite db connection instance
|
||||
Db *sql.DB
|
||||
// datasource name
|
||||
DSN string
|
||||
// mode should be prod or dev
|
||||
mode string
|
||||
DBInstance *sql.DB
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
// NewDB returns a new instance of DB associated with the given datasource name.
|
||||
func NewDB(profile *profile.Profile) *DB {
|
||||
db := &DB{
|
||||
DSN: profile.DSN,
|
||||
mode: profile.Mode,
|
||||
profile: profile,
|
||||
}
|
||||
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.DSN == "" {
|
||||
if db.profile.DSN == "" {
|
||||
return fmt.Errorf("dsn required")
|
||||
}
|
||||
|
||||
// Connect to the database.
|
||||
sqlDB, err := sql.Open("sqlite3", db.DSN)
|
||||
// Connect to the database without foreign_key.
|
||||
sqliteDB, err := sql.Open("sqlite3", db.profile.DSN+"?cache=shared&_foreign_keys=0&_journal_mode=WAL")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.DSN, err)
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
|
||||
}
|
||||
db.DBInstance = sqliteDB
|
||||
|
||||
db.Db = sqlDB
|
||||
// If db file not exists, we should migrate and seed the database.
|
||||
if _, err := os.Stat(db.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
if err := db.migrate(); err != nil {
|
||||
return fmt.Errorf("failed to migrate: %w", err)
|
||||
}
|
||||
// If mode is dev, then seed the database.
|
||||
if db.mode == "dev" {
|
||||
if err := db.seed(); err != nil {
|
||||
if db.profile.Mode == "dev" {
|
||||
// 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 exists and mode is dev, we should migrate and seed the database.
|
||||
if db.mode == "dev" {
|
||||
if err := db.migrate(); err != nil {
|
||||
return fmt.Errorf("failed to migrate: %w", err)
|
||||
// If db file not exists, we should migrate 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(); err != nil {
|
||||
return fmt.Errorf("failed to seed: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistoryList, err := db.FindMigrationHistoryList(ctx, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find migration history, err: %w", err)
|
||||
}
|
||||
if len(migrationHistoryList) == 0 {
|
||||
_, err := db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||
Version: currentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upsert migration history, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
migrationHistoryVersionList := []string{}
|
||||
for _, migrationHistory := range migrationHistoryList {
|
||||
migrationHistoryVersionList = append(migrationHistoryVersionList, migrationHistory.Version)
|
||||
}
|
||||
sort.Sort(version.SortVersion(migrationHistoryVersionList))
|
||||
latestMigrationHistoryVersion := migrationHistoryVersionList[len(migrationHistoryVersionList)-1]
|
||||
|
||||
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
|
||||
minorVersionList := getMinorVersionList()
|
||||
|
||||
// backup the raw database file before migration
|
||||
rawBytes, err := os.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 := 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")
|
||||
|
||||
println("start migrate")
|
||||
for _, minorVersion := range minorVersionList {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && 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")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (db *DB) migrate() error {
|
||||
err := db.compareMigrationHistory()
|
||||
const (
|
||||
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||
)
|
||||
|
||||
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 compare migration history, err=%w", err)
|
||||
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
|
||||
}
|
||||
|
||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/*.sql", "migration"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
|
||||
// Loop over all migration files and execute them in order.
|
||||
for _, filename := range filenames {
|
||||
if err := db.executeFile(migrationFS, filename); err != nil {
|
||||
return fmt.Errorf("migrate error: name=%q err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) seed() error {
|
||||
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||
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 fmt.Errorf("failed to read ddl files, err: %w", err)
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
migrationStmt := ""
|
||||
|
||||
// Loop over all migration files and execute them in order.
|
||||
for _, filename := range filenames {
|
||||
buf, err := migrationFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read minor version migration file, filename=%s err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
migrationStmt += stmt
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.DBInstance.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)
|
||||
|
||||
// Loop over all seed files and execute them in order.
|
||||
for _, filename := range filenames {
|
||||
if err := db.executeFile(seedFS, filename); err != nil {
|
||||
return fmt.Errorf("seed error: name=%q err=%w", filename, err)
|
||||
buf, err := seedFS.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeFile runs a single seed file within a transaction.
|
||||
func (db *DB) executeFile(FS embed.FS, name string) error {
|
||||
tx, err := db.Db.Begin()
|
||||
// execute runs a single SQL statement within a transaction.
|
||||
func (db *DB) execute(ctx context.Context, stmt string) error {
|
||||
tx, err := db.DBInstance.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Read and execute SQL file.
|
||||
if buf, err := fs.ReadFile(FS, name); err != nil {
|
||||
return err
|
||||
} else if _, err := tx.Exec(string(buf)); 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()
|
||||
}
|
||||
|
||||
// compareMigrationHistory compares migration history data
|
||||
func (db *DB) compareMigrationHistory() error {
|
||||
table, err := findTable(db, "migration_history")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if table == nil {
|
||||
if err := createTable(db, `
|
||||
CREATE TABLE migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
`); err != nil {
|
||||
// minorDirRegexp is a regular expression for minor version directory.
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||
|
||||
func getMinorVersionList() []string {
|
||||
minorVersionList := []string{}
|
||||
|
||||
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if file.IsDir() && minorDirRegexp.MatchString(path) {
|
||||
minorVersionList = append(minorVersionList, file.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
currentVersion := common.Version
|
||||
migrationHistory, err := upsertMigrationHistory(db.Db, currentVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if migrationHistory == nil {
|
||||
return fmt.Errorf("failed to upsert migration history")
|
||||
}
|
||||
sort.Sort(version.SortVersion(minorVersionList))
|
||||
|
||||
return nil
|
||||
return minorVersionList
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
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`;
|
||||
105
store/db/migration/dev/LATEST__SCHEMA.sql
Normal file
105
store/db/migration/dev/LATEST__SCHEMA.sql
Normal file
@@ -0,0 +1,105 @@
|
||||
-- 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)
|
||||
);
|
||||
|
||||
-- tag
|
||||
CREATE TABLE tag (
|
||||
name TEXT NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
UNIQUE(name, creator_id)
|
||||
);
|
||||
|
||||
-- activity
|
||||
CREATE TABLE activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
|
||||
payload TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
9
store/db/migration/prod/0.10/00__activity.sql
Normal file
9
store/db/migration/prod/0.10/00__activity.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- activity
|
||||
CREATE TABLE activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
|
||||
payload TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
53
store/db/migration/prod/0.2/00__user_role.sql
Normal file
53
store/db/migration/prod/0.2/00__user_role.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- change user role field from "OWNER"/"USER" to "HOST"/"USER".
|
||||
|
||||
PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS _user_old;
|
||||
|
||||
ALTER TABLE user RENAME TO _user_old;
|
||||
|
||||
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 (
|
||||
id, created_ts, updated_ts, row_status,
|
||||
email, name, password_hash, open_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status,
|
||||
email,
|
||||
name,
|
||||
password_hash,
|
||||
open_id
|
||||
FROM
|
||||
_user_old;
|
||||
|
||||
UPDATE
|
||||
user
|
||||
SET
|
||||
role = 'HOST'
|
||||
WHERE
|
||||
id IN (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
_user_old
|
||||
WHERE
|
||||
role = 'OWNER'
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS _user_old;
|
||||
|
||||
PRAGMA foreign_keys = on;
|
||||
1
store/db/migration/prod/0.2/01__memo_visibility.sql
Normal file
1
store/db/migration/prod/0.2/01__memo_visibility.sql
Normal file
@@ -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;
|
||||
9
store/db/migration/prod/0.4/00__user_setting.sql
Normal file
9
store/db/migration/prod/0.4/00__user_setting.sql
Normal file
@@ -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,21 +1,27 @@
|
||||
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 ('OWNER', 'USER')) DEFAULT 'USER',
|
||||
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
|
||||
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
|
||||
@@ -29,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,
|
||||
@@ -37,13 +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', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo', 100);
|
||||
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
|
||||
@@ -57,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,
|
||||
@@ -68,10 +84,13 @@ CREATE TABLE memo_organizer (
|
||||
UNIQUE(memo_id, user_id)
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo_organizer', 100);
|
||||
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 (
|
||||
@@ -85,10 +104,11 @@ CREATE TABLE shortcut (
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('shortcut', 100);
|
||||
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
|
||||
@@ -102,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,
|
||||
@@ -109,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', 100);
|
||||
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
|
||||
@@ -131,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;
|
||||
10
store/db/migration/prod/0.5/01__memo_resource.sql
Normal file
10
store/db/migration/prod/0.5/01__memo_resource.sql
Normal file
@@ -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)
|
||||
);
|
||||
7
store/db/migration/prod/0.5/02__system_setting.sql
Normal file
7
store/db/migration/prod/0.5/02__system_setting.sql
Normal file
@@ -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 '';
|
||||
55
store/db/migration/prod/0.6/00__recreate_triggers.sql
Normal file
55
store/db/migration/prod/0.6/00__recreate_triggers.sql
Normal file
@@ -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;
|
||||
147
store/db/migration/prod/0.7/00__remove_fk.sql
Normal file
147
store/db/migration/prod/0.7/00__remove_fk.sql
Normal file
@@ -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;
|
||||
4
store/db/migration/prod/0.7/01__remove_triggers.sql
Normal file
4
store/db/migration/prod/0.7/01__remove_triggers.sql
Normal file
@@ -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`;
|
||||
5
store/db/migration/prod/0.8/00__migration_history.sql
Normal file
5
store/db/migration/prod/0.8/00__migration_history.sql
Normal file
@@ -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'))
|
||||
);
|
||||
41
store/db/migration/prod/0.8/01__user_username.sql
Normal file
41
store/db/migration/prod/0.8/01__user_username.sql
Normal file
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user