Compare commits
941 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d52c4609 | ||
|
|
2b3f31a204 | ||
|
|
94332ee597 | ||
|
|
aa6dd044ba | ||
|
|
aa9aa31c42 | ||
|
|
bbdb1fb39d | ||
|
|
b3f3594cd5 | ||
|
|
c0504fe62f | ||
|
|
0bc23c796d | ||
|
|
011b291e05 | ||
|
|
effe173d9c | ||
|
|
bdd440d38f | ||
|
|
fe5e4d9a67 | ||
|
|
66d7a0dce1 | ||
|
|
4f13d7d5a1 | ||
|
|
093ef71c2c | ||
|
|
4a87a90d5b | ||
|
|
17cb901046 | ||
|
|
c0ba47d47d | ||
|
|
1d0cb15854 | ||
|
|
465ff63230 | ||
|
|
04a421d3eb | ||
|
|
2642719769 | ||
|
|
4de97fd91e | ||
|
|
ee93913aac | ||
|
|
e54c4e551c | ||
|
|
743409726f | ||
|
|
2fa82cc231 | ||
|
|
dd4c40d846 | ||
|
|
8acf78dac4 | ||
|
|
eb7c051512 | ||
|
|
5ec90a08a1 | ||
|
|
911275c13b | ||
|
|
704a15896d | ||
|
|
c80b92b346 | ||
|
|
67fc980968 | ||
|
|
05af7dffb8 | ||
|
|
ee068513c6 | ||
|
|
fbb022c877 | ||
|
|
2b90d5eb75 | ||
|
|
edb36f8a92 | ||
|
|
a5bbb26ec8 | ||
|
|
9394e590a8 | ||
|
|
0cb84a9662 | ||
|
|
bb37a11bfb | ||
|
|
d5230acdb3 | ||
|
|
c66e58353a | ||
|
|
6661f35a15 | ||
|
|
5a8eb833be | ||
|
|
1925a1ae7a | ||
|
|
bb9ef6e267 | ||
|
|
923f567e0a | ||
|
|
62809f5300 | ||
|
|
c46207c278 | ||
|
|
289ecffbb1 | ||
|
|
2fdb10a60f | ||
|
|
b2b1693a2f | ||
|
|
b6f5f2405b | ||
|
|
f49f52dc34 | ||
|
|
cd1436add2 | ||
|
|
aefbbda2bf | ||
|
|
768ca9f24a | ||
|
|
fb9c390bc7 | ||
|
|
a4a60f13a0 | ||
|
|
5071c3a904 | ||
|
|
2c52c87c95 | ||
|
|
3161394a83 | ||
|
|
8f6da67f4f | ||
|
|
4d3bfed846 | ||
|
|
2086daf1ae | ||
|
|
a8ab0c6680 | ||
|
|
8b5bf74aa2 | ||
|
|
c0513f85a1 | ||
|
|
8f0db089af | ||
|
|
d705542f44 | ||
|
|
4266fd60b7 | ||
|
|
315b5b8296 | ||
|
|
10699d69f6 | ||
|
|
58da33d3c9 | ||
|
|
20871efb5c | ||
|
|
912e3747ee | ||
|
|
fba0d77e7b | ||
|
|
10dea5cffd | ||
|
|
f7983a2f21 | ||
|
|
8ab428c593 | ||
|
|
fb5f83f43c | ||
|
|
73750890d2 | ||
|
|
3ad7e711be | ||
|
|
b6fd41841b | ||
|
|
b61db2b31a | ||
|
|
cff42566c8 | ||
|
|
e44fabbd31 | ||
|
|
3cdf495bef | ||
|
|
6291acbcee | ||
|
|
1434d6fa1c | ||
|
|
73c55b1a77 | ||
|
|
8780a21fe7 | ||
|
|
3acabd7b38 | ||
|
|
1f826fd652 | ||
|
|
8d266b3d5a | ||
|
|
02b9695882 | ||
|
|
5bba6d0d52 | ||
|
|
52f9c0cbe8 | ||
|
|
d2b4910196 | ||
|
|
9a5cbb2ea7 | ||
|
|
b9a3ea2c44 | ||
|
|
0d826042ad | ||
|
|
f9a84c149c | ||
|
|
798e72f634 | ||
|
|
dec4e6fdb7 | ||
|
|
3cecc20f44 | ||
|
|
86d67450ea | ||
|
|
aa9887715a | ||
|
|
fc1c2095a6 | ||
|
|
a0d216fb2d | ||
|
|
b8471f2ca2 | ||
|
|
b7601272dd | ||
|
|
184dd51122 | ||
|
|
3df02550c4 | ||
|
|
f9b046a32f | ||
|
|
01c850739a | ||
|
|
43bb7792ac | ||
|
|
6c31ace31a | ||
|
|
e9050f4b9b | ||
|
|
a2a45e72ba | ||
|
|
317c2ea369 | ||
|
|
df2ed46ab2 | ||
|
|
979b628f59 | ||
|
|
71924b0397 | ||
|
|
b33c309a56 | ||
|
|
ae785cc56f | ||
|
|
66423b1d18 | ||
|
|
ab21fb203f | ||
|
|
31503d340a | ||
|
|
165635bce9 | ||
|
|
45a639fe3e | ||
|
|
b22a334b86 | ||
|
|
4be12d828d | ||
|
|
683a0a4888 | ||
|
|
769eb4bbd5 | ||
|
|
10921b34fc | ||
|
|
ede1ec5344 | ||
|
|
6f6403165c | ||
|
|
43b54a05ca | ||
|
|
b1b5bd0782 | ||
|
|
b2a4c5b971 | ||
|
|
b86d5053f2 | ||
|
|
aa4fd1c80a | ||
|
|
ed48d6ca75 | ||
|
|
97a8c74e22 | ||
|
|
8d79647974 | ||
|
|
07cd115048 | ||
|
|
e227e5acd6 | ||
|
|
2fb3eba233 | ||
|
|
dde0e988f8 | ||
|
|
32a9c2e85c | ||
|
|
5bd4e81ec5 | ||
|
|
d428d535f5 | ||
|
|
3b1d566973 | ||
|
|
659afb6a50 | ||
|
|
59c490b5d3 | ||
|
|
f1af71d3f3 | ||
|
|
2511fd6abb | ||
|
|
b2663b8ec0 | ||
|
|
eee6346bec | ||
|
|
6edee6b783 | ||
|
|
e526d0b4c8 | ||
|
|
37c6ccd29d | ||
|
|
b0c6a6defa | ||
|
|
3badd7f7a3 | ||
|
|
96ba030b03 | ||
|
|
54d421f93b | ||
|
|
9495179f96 | ||
|
|
32f629bac9 | ||
|
|
a9650b6780 | ||
|
|
08e016e8b0 | ||
|
|
bcffb4c8fc | ||
|
|
985af20b5b | ||
|
|
0c8e4f20c3 | ||
|
|
86cc72f27a | ||
|
|
bf26cb6699 | ||
|
|
928727c149 | ||
|
|
db9d878fe2 | ||
|
|
8663fb4134 | ||
|
|
7464decb8e | ||
|
|
7d5df4cfba | ||
|
|
03f0544ff4 | ||
|
|
ca5a215fc9 | ||
|
|
4ba44d7a16 | ||
|
|
476d797191 | ||
|
|
1714e7c876 | ||
|
|
a3acf85199 | ||
|
|
6d0631b494 | ||
|
|
d465172509 | ||
|
|
f2089c96ca | ||
|
|
e9e2a1eb0e | ||
|
|
51b2682dd0 | ||
|
|
d0bd143e17 | ||
|
|
aab42fb630 | ||
|
|
2389f7b39b | ||
|
|
503b7d8249 | ||
|
|
87f7c500c1 | ||
|
|
69209f75d3 | ||
|
|
8b2bae00d8 | ||
|
|
cf8516d889 | ||
|
|
435abde829 | ||
|
|
7d5cd6c825 | ||
|
|
47db995a54 | ||
|
|
de11905441 | ||
|
|
4ee82c37b0 | ||
|
|
4bf87a11c9 | ||
|
|
bf668edcae | ||
|
|
ac62a5613a | ||
|
|
abc0f70503 | ||
|
|
5781077cfa | ||
|
|
90cb1ec918 | ||
|
|
485b326c96 | ||
|
|
45021411ae | ||
|
|
58c34a2548 | ||
|
|
39f1bb34f0 | ||
|
|
5c4c1b6a65 | ||
|
|
5d29e7e0d1 | ||
|
|
89652c0ae3 | ||
|
|
a96e5e099c | ||
|
|
577b904aa0 | ||
|
|
0b5abe9b05 | ||
|
|
1053fccc9b | ||
|
|
4795c74724 | ||
|
|
8f611098f7 | ||
|
|
39ad13cbad | ||
|
|
eda5d2dd90 | ||
|
|
f9d67996e5 | ||
|
|
34541421ed | ||
|
|
a9eacc24d2 | ||
|
|
eae7321de3 | ||
|
|
16b9ea1dbb | ||
|
|
c623535109 | ||
|
|
9996af93f8 | ||
|
|
43f8900cdd | ||
|
|
cebf64a4bb | ||
|
|
4ea2f28fd4 | ||
|
|
a63527c086 | ||
|
|
f725dce3a9 | ||
|
|
134ceefe19 | ||
|
|
7cd5a84f33 | ||
|
|
af46c54b15 | ||
|
|
ef08cf6fa8 | ||
|
|
2bd6f6c23d | ||
|
|
f9f5d121c2 | ||
|
|
338ade3090 | ||
|
|
65e14f78f3 | ||
|
|
f7982d89e8 | ||
|
|
e273b30e08 | ||
|
|
677a0afeee | ||
|
|
0d1fd3461a | ||
|
|
551ee7014c | ||
|
|
b74049bb37 | ||
|
|
67ff633a3f | ||
|
|
b69bc02b27 | ||
|
|
0a8bfe6bf1 | ||
|
|
a2daf5a404 | ||
|
|
b582dd9b3e | ||
|
|
c30e1a96aa | ||
|
|
162900a9c5 | ||
|
|
e1036e8fe7 | ||
|
|
23678302ba | ||
|
|
9c7920049f | ||
|
|
072f4bff70 | ||
|
|
b7daf09bed | ||
|
|
6f80343706 | ||
|
|
7f83aa828a | ||
|
|
aed749a86d | ||
|
|
68a1a8664a | ||
|
|
2a7dd080ef | ||
|
|
df52d48a31 | ||
|
|
4620003d9a | ||
|
|
a43df045d7 | ||
|
|
43b5b09714 | ||
|
|
7c4865ade9 | ||
|
|
3f05eff482 | ||
|
|
414a187bea | ||
|
|
d987ebda63 | ||
|
|
ca4084fc35 | ||
|
|
fe3ed2eefd | ||
|
|
8f037bdde0 | ||
|
|
46c1f7b911 | ||
|
|
d47109fd5c | ||
|
|
d19eb061b1 | ||
|
|
2bccbb72d8 | ||
|
|
a903680487 | ||
|
|
ccddb8a0fd | ||
|
|
234defc719 | ||
|
|
2e09013bb7 | ||
|
|
843abacdef | ||
|
|
60a7d0a87d | ||
|
|
86edc377d1 | ||
|
|
dada4805c4 | ||
|
|
694669187f | ||
|
|
9bf925cced | ||
|
|
cfc898bda8 | ||
|
|
ebb352782f | ||
|
|
c108e9bc76 | ||
|
|
4e59097013 | ||
|
|
82a98a3374 | ||
|
|
9a178dd3de | ||
|
|
c8e84dcb62 | ||
|
|
5cc8320f14 | ||
|
|
0bdad8b4fe | ||
|
|
b899a3bf08 | ||
|
|
697aa5bb66 | ||
|
|
27ccdb4c73 | ||
|
|
c1de4d03d0 | ||
|
|
af0d3db299 | ||
|
|
f3c50617b5 | ||
|
|
4b1779bf38 | ||
|
|
590d2b7361 | ||
|
|
0c31926fd4 | ||
|
|
b753f5fa0e | ||
|
|
8b23b16b5c | ||
|
|
ef45e36d4d | ||
|
|
6b04113f61 | ||
|
|
6135e29936 | ||
|
|
af9d787efe | ||
|
|
6172fc0a8c | ||
|
|
ddca7a1a22 | ||
|
|
aa122a394d | ||
|
|
09600cf561 | ||
|
|
53ae724620 | ||
|
|
2d905a5550 | ||
|
|
144f2650a2 | ||
|
|
4b9213f358 | ||
|
|
7ad296b74b | ||
|
|
7f53c90128 | ||
|
|
6f08b30986 | ||
|
|
0f26dfd8a0 | ||
|
|
cb0cce0c09 | ||
|
|
b507d71ddd | ||
|
|
c047b0a27e | ||
|
|
c18d2d3169 | ||
|
|
b6429aa7cf | ||
|
|
4342c47189 | ||
|
|
33884c3fd1 | ||
|
|
b9b3bde750 | ||
|
|
42e3ab4865 | ||
|
|
e15026749c | ||
|
|
1510389709 | ||
|
|
e27e539aa3 | ||
|
|
5bbf2ef8d4 | ||
|
|
da0da0ab09 | ||
|
|
36397497a3 | ||
|
|
28dc5b0e2e | ||
|
|
c320c556bc | ||
|
|
7bb19b6329 | ||
|
|
0b75785b5e | ||
|
|
93f7e111ba | ||
|
|
4c95073963 | ||
|
|
8c80e70696 | ||
|
|
0e996b579d | ||
|
|
11857bbe9d | ||
|
|
39238a61d7 | ||
|
|
97d186c5d6 | ||
|
|
6305d5b08c | ||
|
|
cada455745 | ||
|
|
90239f9827 | ||
|
|
a7c0620b3e | ||
|
|
07e63ccba6 | ||
|
|
09d5945e2e | ||
|
|
c9fad14bf7 | ||
|
|
57f51e4e43 | ||
|
|
588f05dfde | ||
|
|
6d8031b0a2 | ||
|
|
5d64068456 | ||
|
|
593c3bc18a | ||
|
|
dddad809a0 | ||
|
|
0182e2f350 | ||
|
|
ba2cff0977 | ||
|
|
d090983954 | ||
|
|
75bf3ec2f7 | ||
|
|
0d4e9514a1 | ||
|
|
63741bd8c5 | ||
|
|
b776938146 | ||
|
|
ec28485e69 | ||
|
|
ff81bcc5d8 | ||
|
|
2b44ab2baa | ||
|
|
61c876961c | ||
|
|
ec5c149a8a | ||
|
|
267cd948bf | ||
|
|
175e3fe56a | ||
|
|
05e5376673 | ||
|
|
0286721429 | ||
|
|
654709b00f | ||
|
|
b4194c5bc3 | ||
|
|
6f648b2e2f | ||
|
|
2cab28a229 | ||
|
|
8fd4ae7bc8 | ||
|
|
de43341a31 | ||
|
|
d31d1bb8d9 | ||
|
|
e42ac5de3b | ||
|
|
d3172f1258 | ||
|
|
3804594829 | ||
|
|
79b7585e35 | ||
|
|
a93424be4a | ||
|
|
746eaa8a92 | ||
|
|
035e1d140b | ||
|
|
ed91764fd5 | ||
|
|
2ea34bcb77 | ||
|
|
2ee532b313 | ||
|
|
fbd0a59938 | ||
|
|
7b57627044 | ||
|
|
4975f3e1c1 | ||
|
|
5662f86aae | ||
|
|
3ac70a772c | ||
|
|
114b6c4b42 | ||
|
|
c79123b178 | ||
|
|
9dadc84f21 | ||
|
|
95fd3fc930 | ||
|
|
ffc2895a6c | ||
|
|
84e9caeb71 | ||
|
|
3134fc437d | ||
|
|
54efefbe39 | ||
|
|
13ac6b064d | ||
|
|
5679d3876d | ||
|
|
90863c8d1c | ||
|
|
4880935844 | ||
|
|
031c0f76ee | ||
|
|
0b4fd797a3 | ||
|
|
3507034427 | ||
|
|
74d89ee9a6 | ||
|
|
ef4d214a9e | ||
|
|
95fd2298e7 | ||
|
|
78b87d60f0 | ||
|
|
89dd7b75c7 | ||
|
|
615da555a5 | ||
|
|
39feb32ba9 | ||
|
|
d6564bbfbd | ||
|
|
cd71b194e6 | ||
|
|
4e38a53971 | ||
|
|
cf745ec73a | ||
|
|
046d642a87 | ||
|
|
d56e605e35 | ||
|
|
24a0a576e8 | ||
|
|
d8ee38573b | ||
|
|
0e8ac365a5 | ||
|
|
e1c6201c7c | ||
|
|
6a0704b183 | ||
|
|
526e32a5f0 | ||
|
|
83bee48279 | ||
|
|
5d87c5dbbc | ||
|
|
34af734d01 | ||
|
|
82ec6c6ac6 | ||
|
|
9718871610 | ||
|
|
ff2162cbe9 | ||
|
|
3298e3f4d8 | ||
|
|
7e142dd5c6 | ||
|
|
7aa085d19f | ||
|
|
84a4698e97 | ||
|
|
c919c556ca | ||
|
|
a6d0b9edff | ||
|
|
8e8113af50 | ||
|
|
c9bca05daa | ||
|
|
90eb5c71e9 | ||
|
|
4cbdcf8354 | ||
|
|
d59e0c7bd1 | ||
|
|
6cdf71dcb4 | ||
|
|
a051ccb2c6 | ||
|
|
b1e79f846c | ||
|
|
08c8ce757e | ||
|
|
6e2088f5b6 | ||
|
|
6f96b298a3 | ||
|
|
b38957d374 | ||
|
|
8e4d9f23ad | ||
|
|
8d04e56572 | ||
|
|
3a1095dcb3 | ||
|
|
f12beb3fd2 | ||
|
|
f3bc257a87 | ||
|
|
0cba2cea6c | ||
|
|
ee1a984abf | ||
|
|
7db4615272 | ||
|
|
fbd7470cd6 | ||
|
|
19b76634e6 | ||
|
|
5dff05f791 | ||
|
|
81016e5643 | ||
|
|
3e911db0ca | ||
|
|
f36ba0db6b | ||
|
|
0481188a66 | ||
|
|
839b1b5927 | ||
|
|
44b851d86a | ||
|
|
eb4c7117a1 | ||
|
|
6464851d2e | ||
|
|
fba806d24a | ||
|
|
c5cf0081ae | ||
|
|
47a36f7c26 | ||
|
|
edb5c2a6b4 | ||
|
|
a00cdb4f7f | ||
|
|
f8f9422491 | ||
|
|
da077dc8bf | ||
|
|
4d96077769 | ||
|
|
e0b68d5470 | ||
|
|
844d768fd3 | ||
|
|
7117198e59 | ||
|
|
10f38ad57d | ||
|
|
e4823e262a | ||
|
|
f757a882d9 | ||
|
|
4897e6e6c8 | ||
|
|
d609d5527c | ||
|
|
cdd7269ff4 | ||
|
|
facbc1269b | ||
|
|
935f81c03f | ||
|
|
0ecf67188a | ||
|
|
5bdd05960e | ||
|
|
62402ef8b4 | ||
|
|
91a5f606df | ||
|
|
8b584b54e4 | ||
|
|
6a4a57aa65 | ||
|
|
b25272346c | ||
|
|
fe52158d8e | ||
|
|
173cf43619 | ||
|
|
013a71234b | ||
|
|
f326f74b76 | ||
|
|
5cd405ea7c | ||
|
|
50d79fc2c2 | ||
|
|
7c5e52591f | ||
|
|
3b6971d648 | ||
|
|
fdb55f1c2b | ||
|
|
15685d664f | ||
|
|
bb78e935a8 | ||
|
|
f4a1064b15 | ||
|
|
09df3deeea | ||
|
|
55ece284da | ||
|
|
b67ae153f9 | ||
|
|
aca4acbc8e | ||
|
|
dde30bde77 | ||
|
|
035c184761 | ||
|
|
44d096c8ed | ||
|
|
708d3e5137 | ||
|
|
f3b5840c3e | ||
|
|
e158be2ce2 | ||
|
|
9842338d25 | ||
|
|
cbe0f58f2e | ||
|
|
10073b8f98 | ||
|
|
c2ae231293 | ||
|
|
64e328f6b3 | ||
|
|
33af412423 | ||
|
|
fc385d49a8 | ||
|
|
08982260fb | ||
|
|
4689af7768 | ||
|
|
65baffe44e | ||
|
|
16ac2b8144 | ||
|
|
8ad22cc147 | ||
|
|
6c61ccc3e4 | ||
|
|
3289b9d688 | ||
|
|
4518bfdbce | ||
|
|
01b4502c45 | ||
|
|
68a55a8617 | ||
|
|
4a9715d77f | ||
|
|
d229ef18a8 | ||
|
|
cdd40511ea | ||
|
|
2019fb86b7 | ||
|
|
9ee8d51852 | ||
|
|
bbdad3f911 | ||
|
|
7505616e3f | ||
|
|
d795171461 | ||
|
|
f055126a69 | ||
|
|
970205e56d | ||
|
|
010c33f18c | ||
|
|
3f6df9d2a0 | ||
|
|
1da4a536ba | ||
|
|
9bd446e822 | ||
|
|
5efd4f5b71 | ||
|
|
5d7e5a3cad | ||
|
|
06fa1e5636 | ||
|
|
ac1b39070a | ||
|
|
8040d0de1f | ||
|
|
3282c62b22 | ||
|
|
f91c4b2fbf | ||
|
|
e2e0a2ce9a | ||
|
|
edc61f42d9 | ||
|
|
80e8682a5f | ||
|
|
c5de08c00b | ||
|
|
7fded3473a | ||
|
|
b7788d0e5e | ||
|
|
bdae76cadb | ||
|
|
2cca08a2cd | ||
|
|
fd863c8a4a | ||
|
|
dc8189335f | ||
|
|
73742f886e | ||
|
|
617273a9a4 | ||
|
|
8f03aebcdc | ||
|
|
4466003203 | ||
|
|
dc91c49713 | ||
|
|
8853cac38d | ||
|
|
874a554829 | ||
|
|
9800d5df07 | ||
|
|
82988ebe2e | ||
|
|
39b1ac1705 | ||
|
|
e7a08e5932 | ||
|
|
b8de26f515 | ||
|
|
11327ed215 | ||
|
|
fbbc69f80a | ||
|
|
e4e7e57580 | ||
|
|
c5bed1e7a4 | ||
|
|
83d794acb4 | ||
|
|
635d05e217 | ||
|
|
8e3179ba42 | ||
|
|
5f7d056a77 | ||
|
|
5147586776 | ||
|
|
2a00978441 | ||
|
|
b8a8174d0c | ||
|
|
89f537c4e2 | ||
|
|
d09d57dbe1 | ||
|
|
c09bece154 | ||
|
|
0c0b60f86c | ||
|
|
77cbabf20b | ||
|
|
f951a2bd06 | ||
|
|
7b5634b439 | ||
|
|
b704a2c30a | ||
|
|
fbea72c239 | ||
|
|
5565b02991 | ||
|
|
876e45b55c | ||
|
|
8afd8b4f21 | ||
|
|
6377815540 | ||
|
|
b5ae9bbe90 | ||
|
|
4708057b8b | ||
|
|
edcadf1c3c | ||
|
|
5c3d5b7ec4 | ||
|
|
6226f4b445 | ||
|
|
0688d1192c | ||
|
|
f1d30f03c1 | ||
|
|
f53fed14f7 | ||
|
|
e5fce6f1dd | ||
|
|
28e8f8dc2a | ||
|
|
af52759a1b | ||
|
|
d51c4e6d3e | ||
|
|
774cba8658 | ||
|
|
4c789dd99a | ||
|
|
bde8217a6d | ||
|
|
de90d79fbd | ||
|
|
089f4fdde8 | ||
|
|
e985da4492 | ||
|
|
1fc8835018 | ||
|
|
2ed547a735 | ||
|
|
5e4d4a7161 | ||
|
|
d9065778aa | ||
|
|
749583a11e | ||
|
|
9a54fcaa52 | ||
|
|
271981d0f0 | ||
|
|
5231f8d541 | ||
|
|
703d686d5e | ||
|
|
989f0d323a | ||
|
|
1bbd06cb28 | ||
|
|
26840ede07 | ||
|
|
1a4a317faf | ||
|
|
d30a29e89e | ||
|
|
b23fc23788 | ||
|
|
97b2eafdae | ||
|
|
a4bbadaa88 | ||
|
|
931c546257 | ||
|
|
32aafe5f1b | ||
|
|
a828a16890 | ||
|
|
02adeab97f | ||
|
|
676a1efa7e | ||
|
|
7721e49a78 | ||
|
|
678ba4f73d | ||
|
|
d68c671d48 | ||
|
|
8093181585 | ||
|
|
a12ef5fd60 | ||
|
|
9bdad2aeae | ||
|
|
db995622a9 | ||
|
|
d39e40a4ba | ||
|
|
e88ef59b7b | ||
|
|
bfe9e322aa | ||
|
|
2ffb2dc8b8 | ||
|
|
9e6f1d4025 | ||
|
|
f35dfcea36 | ||
|
|
b14e790fbb | ||
|
|
d9c6cf4b5c | ||
|
|
6ba708143d | ||
|
|
eac3cade5c | ||
|
|
dc78ef18af | ||
|
|
62b0ee5e2b | ||
|
|
ca06b2ffab | ||
|
|
98b3b2da71 | ||
|
|
f93fe52835 | ||
|
|
f8c4329f36 | ||
|
|
3dce804566 | ||
|
|
1022c23d59 | ||
|
|
12bca3ec43 | ||
|
|
bb7ebf1e81 | ||
|
|
cb5f2f8bce | ||
|
|
35e7cb51a7 | ||
|
|
e4154d0723 | ||
|
|
8ee4598a9c | ||
|
|
544149637f | ||
|
|
50348ab7d6 | ||
|
|
8d735df6a8 | ||
|
|
32aac46ff0 | ||
|
|
531b95ca33 | ||
|
|
5d2633c49f | ||
|
|
9266139c25 | ||
|
|
f8be031d08 | ||
|
|
ef797c2847 | ||
|
|
807c96d68b | ||
|
|
9eda2ef204 | ||
|
|
338ef83f73 | ||
|
|
629078637b | ||
|
|
d71b444de8 | ||
|
|
00c2f3a979 | ||
|
|
47d6a60f4c | ||
|
|
d30d0c6960 | ||
|
|
e1703f1146 | ||
|
|
dcc14a1493 | ||
|
|
da4556bf48 | ||
|
|
4b60767b0a | ||
|
|
3a236746c4 | ||
|
|
b6a25939b2 | ||
|
|
fba0ee18cc | ||
|
|
659c750645 | ||
|
|
ec25acbfbb | ||
|
|
8a27933d44 | ||
|
|
767f6a49df | ||
|
|
d90cacb33a | ||
|
|
dd927972af | ||
|
|
d4ba4648b4 | ||
|
|
af5bd11c8f | ||
|
|
54627d6647 | ||
|
|
c2d20536a3 | ||
|
|
56460b57d5 | ||
|
|
42874d3c58 | ||
|
|
fd766552f7 | ||
|
|
92355bd765 | ||
|
|
3e083ff453 | ||
|
|
243587450b | ||
|
|
e1cab9e44c | ||
|
|
b165545f8e | ||
|
|
2a30fd56d8 | ||
|
|
9ffb41f047 | ||
|
|
c6d04f8a94 | ||
|
|
3d4eb3ed64 | ||
|
|
4593801d22 | ||
|
|
ca08b610d7 | ||
|
|
5c0cfeb6a1 | ||
|
|
4f8d57f052 | ||
|
|
4bd6410e86 | ||
|
|
27046d1556 | ||
|
|
0eb0169701 | ||
|
|
b328d4bf30 | ||
|
|
e136b24314 | ||
|
|
fec1fafb3e | ||
|
|
6296a78ec4 | ||
|
|
d5bfb689c4 | ||
|
|
0f7fedc66e | ||
|
|
3468a901f4 | ||
|
|
666b98db07 | ||
|
|
980b503dd7 | ||
|
|
069514469d | ||
|
|
e6cbaa88c7 | ||
|
|
a6af81c800 | ||
|
|
28ba9d0c02 | ||
|
|
132a656e90 | ||
|
|
726d5163db | ||
|
|
aac0d26614 | ||
|
|
f52ffac078 | ||
|
|
00376401fc | ||
|
|
785e49b7d5 | ||
|
|
5c64eaeb4e | ||
|
|
50dabc2f48 | ||
|
|
9409866bb9 | ||
|
|
4d28c60cb8 | ||
|
|
c36831ccea | ||
|
|
72f3ad447e | ||
|
|
956a7d8966 | ||
|
|
7effc2c6d6 | ||
|
|
3915f83883 | ||
|
|
e3ca844893 | ||
|
|
5a41b1dc1e | ||
|
|
2cb60d56c0 | ||
|
|
e93e0f3675 | ||
|
|
b719e6019a | ||
|
|
3331dcd385 | ||
|
|
a34aa85c39 | ||
|
|
71f16228ce | ||
|
|
e62912a267 | ||
|
|
bf173ccacd | ||
|
|
a8cf1aab4e | ||
|
|
c44e9d266d | ||
|
|
aa43585622 | ||
|
|
28d6f5d670 | ||
|
|
3823c9c2ee | ||
|
|
4ee24754fa | ||
|
|
c56dc34a94 | ||
|
|
94f9f83cf2 | ||
|
|
0a254dc22c | ||
|
|
4af1db0b18 | ||
|
|
c120f78a67 | ||
|
|
f79128866a | ||
|
|
2479a81bde | ||
|
|
c0ecd8ab8a | ||
|
|
b943d46930 | ||
|
|
ad1fe2ca1e | ||
|
|
c8e4848620 | ||
|
|
3576d40dc9 | ||
|
|
e3880f50e7 | ||
|
|
e7fa6fac11 | ||
|
|
5edea2cda8 | ||
|
|
9cbc0debe9 | ||
|
|
1d93a5b691 | ||
|
|
b20b92c9c2 | ||
|
|
2f67623520 | ||
|
|
83cbc57838 | ||
|
|
406040d9d4 | ||
|
|
58b5956a15 | ||
|
|
08166e2044 | ||
|
|
4b7d34fc4f | ||
|
|
53c62f6e9b | ||
|
|
eb30c5320d | ||
|
|
b838d955f5 | ||
|
|
20dfc30d63 | ||
|
|
e6229d18ae | ||
|
|
3f79af5171 | ||
|
|
17fca67b2e | ||
|
|
78dce8076d | ||
|
|
dce1376b70 | ||
|
|
e101da200b | ||
|
|
01b067e02f | ||
|
|
a3ff28d8fd | ||
|
|
519bc2f702 | ||
|
|
a36d31e360 | ||
|
|
d884d29d13 | ||
|
|
b753ce408d | ||
|
|
77388d0c55 | ||
|
|
16279ae70b | ||
|
|
8cc652a91d | ||
|
|
e376242793 | ||
|
|
1625502ab7 | ||
|
|
119f7851de | ||
|
|
c3c94e032f | ||
|
|
87c8805dd7 | ||
|
|
b082e1bc7b | ||
|
|
c109a0cecb | ||
|
|
1985abf7b3 | ||
|
|
c65787f9e8 | ||
|
|
075c2141c6 | ||
|
|
801a852768 | ||
|
|
2a9ac60b85 | ||
|
|
cfadf14e44 | ||
|
|
b10f25fdac | ||
|
|
8117e56e8d | ||
|
|
6cc3ad3081 | ||
|
|
aa145ed695 | ||
|
|
d4879dc6f1 | ||
|
|
64faf21fdc | ||
|
|
c0a8410faa | ||
|
|
30845c1980 | ||
|
|
a78f9800d2 | ||
|
|
a1680c8751 | ||
|
|
3939647f8f | ||
|
|
bf37cd732b | ||
|
|
a1bd9be030 | ||
|
|
0fa80e5d22 | ||
|
|
ae4d863f7a | ||
|
|
f3f2f606eb | ||
|
|
e590626c40 | ||
|
|
f1d4984f7b | ||
|
|
c776c34426 | ||
|
|
2a3933b436 | ||
|
|
538454e1af | ||
|
|
4ccf046521 | ||
|
|
0330b3dc88 | ||
|
|
f28af89887 | ||
|
|
73ec6dd809 | ||
|
|
c95f15b2b9 | ||
|
|
698335daff | ||
|
|
51b524e28d | ||
|
|
fcda0498c4 | ||
|
|
49569036c5 | ||
|
|
beb4d7f075 | ||
|
|
3954874e96 | ||
|
|
efe976b262 | ||
|
|
9bf59e339b | ||
|
|
2a93c3cfee | ||
|
|
96d241ff19 | ||
|
|
e5d1c3f7a1 | ||
|
|
40cecd2384 | ||
|
|
48a68c98e8 | ||
|
|
92809e5fbf | ||
|
|
a3b8ec01bc | ||
|
|
2aa4690486 | ||
|
|
5f51bd7ae0 | ||
|
|
dd9e77c102 | ||
|
|
732022e322 | ||
|
|
fb2b3d5e47 | ||
|
|
b887ab3d6f | ||
|
|
a0e3296679 | ||
|
|
359dd77654 | ||
|
|
5a70185cb6 | ||
|
|
72552e3b55 | ||
|
|
7b52fb947f | ||
|
|
f3daaa640d | ||
|
|
86e3a9e72e | ||
|
|
00b4bbde8d | ||
|
|
2f3c50d746 | ||
|
|
523d3975d8 | ||
|
|
aeffcc9348 | ||
|
|
3af308ffa3 | ||
|
|
622a5723ef | ||
|
|
129039904a | ||
|
|
e04ea3f9e1 | ||
|
|
bc6080f1cc | ||
|
|
801f2c06c0 | ||
|
|
c61e11422c | ||
|
|
99c30392ea | ||
|
|
fb0b87973f | ||
|
|
55fc0e600c | ||
|
|
1e1cc157dc | ||
|
|
6c8e4c13bc | ||
|
|
a78edaa16a | ||
|
|
95e243aad5 | ||
|
|
a650394be8 | ||
|
|
58c7dc84a6 | ||
|
|
dc3722b981 | ||
|
|
d9764fa7e8 | ||
|
|
06da69eece | ||
|
|
e73be6ecb3 | ||
|
|
b03d04bd44 | ||
|
|
a4961b7ecc | ||
|
|
5f00684d71 | ||
|
|
c5810f7bd7 | ||
|
|
9bb335fb9a | ||
|
|
963a1f6e2f | ||
|
|
a966b0d85e | ||
|
|
095c78f91e | ||
|
|
f819df0f0f | ||
|
|
d9c2e23b52 | ||
|
|
9bc36e2370 | ||
|
|
8fe1190da5 | ||
|
|
b7b8fd463b | ||
|
|
7efbd8abb4 | ||
|
|
dd8217b89e | ||
|
|
ce128313a8 | ||
|
|
2394629203 | ||
|
|
d946353c5c |
@@ -8,8 +8,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{html, less, js}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
||||
39
.env.example
Normal file
39
.env.example
Normal file
@@ -0,0 +1,39 @@
|
||||
APP_NAME="Lsky Pro"
|
||||
APP_ENV=prod
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=public
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
IGNITION_SHARING_ENABLED=false
|
||||
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
* text=auto
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: https://github.com/wisp-x/lsky-pro#-%E6%8D%90%E8%B5%A0
|
||||
35
.github/workflows/main.yml
vendored
Normal file
35
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Lsky-pro
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
lsky-pro-tests:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
|
||||
with:
|
||||
php-version: '8.0'
|
||||
- uses: actions/checkout@v2
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
run: |
|
||||
mkdir -p database
|
||||
touch database/database.sqlite
|
||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: database/database.sqlite
|
||||
run: vendor/bin/phpunit
|
||||
50
.gitignore
vendored
50
.gitignore
vendored
@@ -1,32 +1,20 @@
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
.idea
|
||||
|
||||
runtime
|
||||
/config/db.php
|
||||
/installed.lock
|
||||
/upgrading.lock
|
||||
/*.zip
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/js/custom.js
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
.env.backup
|
||||
.phpunit.result.cache
|
||||
docker-compose.override.yml
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.idea
|
||||
/.vscode
|
||||
/public/i
|
||||
|
||||
13
.styleci.yml
Normal file
13
.styleci.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
php:
|
||||
preset: laravel
|
||||
version: 8
|
||||
disabled:
|
||||
- no_unused_imports
|
||||
finder:
|
||||
not-name:
|
||||
- index.php
|
||||
js:
|
||||
finder:
|
||||
not-name:
|
||||
- webpack.mix.js
|
||||
css: true
|
||||
2
LICENSE
2
LICENSE
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Lsky Pro Copyright (C) 2018 熊二哈
|
||||
Lsky Pro Copyright (C) 2018 Wisp X
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
187
README.md
187
README.md
@@ -1,118 +1,101 @@
|
||||
<p align="center">
|
||||
<a href="./public/static/app/images/logo.png" target="_blank">
|
||||
<img width="300" src="./public/static/app/images/logo.png">
|
||||
</a>
|
||||
</p>
|
||||
<img align="right" width="100" src="https://avatars.githubusercontent.com/u/100565733?s=200" alt="Lsky Pro Logo"/>
|
||||
|
||||
# Lsky Pro - Your photo album on the cloud.
|
||||
<h1 align="left"><a href="https://www.lsky.pro">Lsky Pro</a></h1>
|
||||
|
||||
[官网](https://www.lsky.pro)
|
||||
[手册](https://www.kancloud.cn/wispx/lsky-pro)
|
||||
☁ Your photo album on the cloud.
|
||||
|
||||
[](https://github.com/wisp-x/lsky-pro/blob/master/LICENSE)
|
||||
[](http://php.net)
|
||||
[](https://github.com/wisp-x/lsky-pro)
|
||||
[](https://gitter.im/wisp-x/lsky-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge&style=flat-square)
|
||||
[](http://php.net)
|
||||
[](https://github.com/lsky-org/lsky-pro/releases)
|
||||
[](https://github.com/lsky-org/lsky-pro/issues)
|
||||
[](https://github.com/lsky-org/lsky-pro)
|
||||
[](https://github.com/lsky-org/lsky-pro)
|
||||
[](https://github.com/lsky-org/lsky-pro/commits/dev)
|
||||
[](https://github.com/lsky-org/lsky-pro/blob/master/LICENSE)
|
||||
|
||||
> master分支为最新版,其他版本请点击[这里](https://github.com/wisp-x/lsky-pro/releases)
|
||||
[官网](https://www.lsky.pro) ·
|
||||
[文档](https://docs.lsky.pro) ·
|
||||
[社区](https://github.com/lsky-org/lsky-pro/discussions) ·
|
||||
[演示](https://pic.vv1234.cn) ·
|
||||
[Telegram 群组](https://t.me/lsky_pro)
|
||||
|
||||
> 发现bug可发送邮件至邮箱:i@wispx.cn,或提交[issues](https://github.com/wisp-x/lsky-pro/issues),确认bug后我会及时修复,谢谢!
|
||||
> [!WARNING]
|
||||
> 开源版本已停止维护,不再进行新特性更新和 bug 修复。
|
||||
|
||||

|
||||

|
||||
> master 分支为未安装三方拓展的版本,通常包含了最新未发布版本的一些实验性新特性和修复补丁,正式版本请点击 [这里](https://github.com/lsky-org/lsky-pro/releases) 下载。
|
||||
> 发现 bug 请提交 [issues](https://github.com/lsky-org/lsky-pro/issues) (提问前建议阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md))
|
||||
> 有任何想法、建议、或分享,请移步 [社区](https://github.com/lsky-org/lsky-pro/discussions)
|
||||
|
||||
主要特性
|
||||
---
|
||||
- 支持第三方云储存,支持本地、阿里云OSS、腾讯云COS、七牛云、又拍云。
|
||||
- 支持多图上传、拖拽上传、上传预览、全屏预览、页面响应式布局。
|
||||
- 简洁的图片管理功能,支持鼠标右键、单选多选等操作。
|
||||
- 强大的图片预览功能,支持响应式。
|
||||
- 支持全局配置用户初始剩余储存空间、支持单个设置用户剩余储存空间。
|
||||
- 支持一键复制图片外链、二维码扫描链接。
|
||||
- 支持设置上传文件、文件夹路径命名规则。
|
||||
- 支持图片鉴黄功能。
|
||||
- 支持文件夹分类功能。
|
||||
- 对外开放的上传接口。
|
||||

|
||||

|
||||
|
||||
安装需求
|
||||
---
|
||||
* PHP版本 ≥ 5.6(建议使用PHP7+)
|
||||
* mysql版本 ≥ 5.5
|
||||
* mysqli支持
|
||||
* fileinfo拓展
|
||||
* curl拓展
|
||||
* rewrite
|
||||
### 📌 TODO
|
||||
* [x] 支持`本地`等多种第三方云储存 `AWS S3`、`阿里云 OSS`、`腾讯云 COS`、`七牛云`、`又拍云`、`SFTP`、`FTP`、`WebDav`、`Minio`
|
||||
* [x] 多种数据库驱动支持,`MySQL 5.7+`、`PostgreSQL 9.6+`、`SQLite 3.8.8+`、`SQL Server 2017+`
|
||||
* [x] 支持配置使用多种缓存驱动,`Memcached`、`Redis`、`DynamoDB`、等其他关系型数据库,默认以文件的方式缓存
|
||||
* [x] 多图上传、拖拽上传、粘贴上传、动态设置策略上传、复制、一键复制链接
|
||||
* [x] 强大的图片管理功能,瀑布流展示,支持鼠标右键、单选多选、重命名等操作
|
||||
* [x] 自由度极高的角色组配置,可以为每个组配置多个储存策略,同时储存策略可以配置多个角色组
|
||||
* [x] 可针对角色组设置上传文件、文件夹路径命名规则、上传频率限制、图片审核等功能
|
||||
* [x] 支持图片水印、文字水印、水印平铺、设置水印位置、X/y 轴偏移量设置、旋转角度等
|
||||
* [x] 支持通过接口上传、管理图片、管理相册
|
||||
* [x] 支持在线增量更新、跨版本更新
|
||||
* [x] 图片广场
|
||||
|
||||
安装教程
|
||||
---
|
||||
1. 下载兰空,上传至web运行环境,解压。
|
||||
2. 设置运行目录为 public。
|
||||
3. 配置Rewrite规则:
|
||||
##### Nginx:
|
||||
```
|
||||
location / {
|
||||
if (!-e $request_filename) {
|
||||
rewrite ^(.*)$ /index.php?s=$1 last; break;
|
||||
}
|
||||
}
|
||||
```
|
||||
### 🛠 安装要求
|
||||
- PHP >= 8.0.2
|
||||
- BCMath PHP 扩展
|
||||
- Ctype PHP 扩展
|
||||
- DOM PHP 拓展
|
||||
- Fileinfo PHP 扩展
|
||||
- JSON PHP 扩展
|
||||
- Mbstring PHP 扩展
|
||||
- OpenSSL PHP 扩展
|
||||
- PDO PHP 扩展
|
||||
- Tokenizer PHP 扩展
|
||||
- XML PHP 扩展
|
||||
- Imagick 拓展
|
||||
- exec、shell_exec 函数
|
||||
- readlink、symlink 函数
|
||||
- putenv、getenv 函数
|
||||
- chmod、chown、fileperms 函数
|
||||
|
||||
##### Apache:
|
||||
Apache直接使用.htaccess即可
|
||||
### 😋 鸣谢
|
||||
- [Laravel](https://laravel.com)
|
||||
- [Tailwindcss](https://tailwindcss.com)
|
||||
- [Fontawesome](https://fontawesome.com)
|
||||
- [Echarts](https://echarts.apache.org)
|
||||
- [Intervention/image](https://github.com/Intervention/image)
|
||||
- [league/flysystem](https://flysystem.thephpleague.com)
|
||||
- [overtrue](https://github.com/overtrue)
|
||||
- [Jquery](https://jquery.com)
|
||||
- [jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload)
|
||||
- [Alpinejs](https://alpinejs.dev/)
|
||||
- [Viewer.js](https://github.com/fengyuanchen/viewerjs)
|
||||
- [DragSelect](https://github.com/ThibaultJanBeyer/DragSelect)
|
||||
- [Justified-Gallery](https://github.com/miromannino/Justified-Gallery)
|
||||
- [Clipboard.js](https://github.com/zenorocha/clipboard.js)
|
||||
|
||||
4. 访问首页,未安装自动跳转至安装页面,根据页面提示安装即可。
|
||||
5. 安装完成以后请设置runtime目录0755权限,如果你使用本地存储,public 目录也需要设置为0755权限
|
||||
### 💰 捐赠
|
||||
Lsky Pro 的开发和更新等,都是作者在业余时间独立开发,并免费开源使用,如果您认可我的作品,并且觉得对你有所帮助我愿意接受来自各方面的捐赠😃。
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<th>支付宝</th>
|
||||
<th>微信</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img alt="看不见图片请使用科学上网" src="https://raw.githubusercontent.com/lsky-org/lsky-pro/82988ebe2edd32264d609b26bf9132b3dce7c39e/public/static/app/images/demo/alipay.png"></td>
|
||||
<td><img alt="看不见图片请使用科学上网" src="https://raw.githubusercontent.com/lsky-org/lsky-pro/82988ebe2edd32264d609b26bf9132b3dce7c39e/public/static/app/images/demo/wechat.jpeg"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
如何更新到最新版?
|
||||
---
|
||||
升级过程并不复杂,但也简单粗暴,总共分为四个步骤。
|
||||
1. [下载](https://github.com/wisp-x/lsky-pro/releases)最新版程序并解压到本地。
|
||||
2. 将旧版程序 ```config/db.php``` 文件复制到新版程序的 ```config``` 文件夹,如果你使用的是本地储存策略,文件是储存在本地的,请注意也要将你旧版本的图片资源移动到新程序对应的目录。
|
||||
3. 删除旧版本程序所有文件,上传最新版程序到站点根目录即可(这一步也可以直接覆盖,但会有残留文件,不选择覆盖的话建议先打包备份旧版本)。
|
||||
4. 使用管理员账号登录,访问任意页面会跳转到升级数据库结构页面,根据提示操作即可。
|
||||
### 🤩 Stargazers over time
|
||||
[](https://starchart.cc/lsky-org/lsky-pro)
|
||||
|
||||
如何修改网站运行目录?
|
||||
---
|
||||
默认程序的入口文件在 ```public``` 目录下,所以需要把public目录设置为网站运行目录,这样做是为了:
|
||||
> 入口文件位置的设计是为了让应用部署更安全,```public``` 目录为web可访问目录,其他的文件都可以放到非WEB访问目录下面。
|
||||
### 📧 联系我
|
||||
- Email: i@wispx.cn
|
||||
|
||||
而你如果安装时没有设置,使用 ```http://域名/public``` 的方式来访问站点,会导致css和js等静态资源无法获取。
|
||||
如果你 无法 或 不会 设置运行目录,可以将public目录下的所有文件和文件夹(包括.htaccess文件)移动到根目录
|
||||
(和 ```application``` 文件夹同级)即可,尽管我们不推荐你这么做,这样会导致应用程序核心文件暴露在外。
|
||||
|
||||
移动文件以后,打开根目录 ```index.php``` 文件,修改如下:
|
||||
```php
|
||||
<?php
|
||||
|
||||
// [ 应用入口文件 ]
|
||||
namespace think;
|
||||
|
||||
// 加载基础文件
|
||||
require __DIR__ . '/thinkphp/base.php';
|
||||
|
||||
// 执行应用并响应
|
||||
Container::get('app')->bind('index')->run()->send();
|
||||
```
|
||||
|
||||
注意:请不要使用记事本打开修改,修改完成后保存即可。
|
||||
|
||||
联系我
|
||||
---
|
||||
- QQ:1591788658
|
||||
- Email: 1591788658@qq.com
|
||||
- Blog:[https://www.wispx.cn](https://www.wispx.cn)
|
||||
|
||||
鸣谢
|
||||
---
|
||||
- ThinkPHP
|
||||
- Jquery
|
||||
- BootStrap
|
||||
- Mdui
|
||||
- viewer.js
|
||||
- context.js
|
||||
|
||||
开源许可
|
||||
---
|
||||
### 📃 开源许可
|
||||
[GPL 3.0](https://opensource.org/licenses/GPL-3.0)
|
||||
|
||||
Copyright (c) 2018 Wisp X.
|
||||
Copyright (c) 2018-present Lsky Pro.
|
||||
|
||||
|
||||
113
app/Console/Commands/Install.php
Normal file
113
app/Console/Commands/Install.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Utils;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class Install extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'lsky:install';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Install Lsky Pro.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->signature = implode(' ', [
|
||||
'lsky:install',
|
||||
'{--connection=mysql : Database type}',
|
||||
'{--host=127.0.0.1 : Database connection address}',
|
||||
'{--port=3306 : Database connection port}',
|
||||
'{--database= : Database name}',
|
||||
'{--username=root : Database connection user name}',
|
||||
'{--password=root : Database connection password}',
|
||||
]);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
// 判断是否已经安装
|
||||
if (file_exists(base_path('installed.lock'))) {
|
||||
$this->warn('Already installed. if you want to reinstall, please remove installed.lock file.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$driver = $this->option('connection');
|
||||
$connection = "database.connections.{$driver}";
|
||||
$options = [
|
||||
'connection' => $this->option('connection'),
|
||||
'host' => $this->option('host'),
|
||||
'port' => $this->option('port'),
|
||||
'database' => $this->option('database'),
|
||||
'username' => $this->option('username'),
|
||||
'password' => $this->option('password'),
|
||||
];
|
||||
$configs = array_intersect_key($options, config($connection));
|
||||
|
||||
// 覆盖默认配置
|
||||
Config::set($connection, array_merge(config($connection), $configs));
|
||||
// 设置默认数据库驱动
|
||||
Config::set('database.default', $driver);
|
||||
|
||||
clearstatcache(true);
|
||||
|
||||
try {
|
||||
if ($options['connection'] === 'sqlite' && ! $options['database']) {
|
||||
$options['database'] = database_path('database.sqlite');
|
||||
file_put_contents($options['database'], '');
|
||||
Config::set('database.connections.sqlite.database', $options['database']);
|
||||
}
|
||||
// 执行数据库迁移
|
||||
Artisan::call('migrate:fresh', ['--force' => true], outputBuffer: $this->output);
|
||||
// 填充数据
|
||||
Artisan::call('db:seed', ['--force' => true, '--class' => 'InstallSeeder'], outputBuffer: $this->output);
|
||||
// 更新 env 文件
|
||||
$replaces = collect($options)->transform(fn ($item, $key) => ['DB_'.strtoupper($key) => $item])->collapse();
|
||||
file_put_contents($this->laravel->environmentFilePath(), preg_replace(
|
||||
$replaces->map(fn ($item, $key) => $this->replacementPattern($key, env($key, '')))->values()->toArray(),
|
||||
$replaces->map(fn ($item, $key) => "{$key}={$item}")->values()->toArray(),
|
||||
file_get_contents($this->laravel->environmentFilePath())
|
||||
));
|
||||
// 创建锁文件
|
||||
file_put_contents(base_path('installed.lock'), '');
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn("Installation error!\n");
|
||||
$this->error($e->getMessage());
|
||||
Utils::e($e, '执行数据库安装程序时出现异常');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('Install success!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function replacementPattern(string $name, string $value): string
|
||||
{
|
||||
$escaped = preg_quote('='.$value, '/');
|
||||
|
||||
return "/^{$name}{$escaped}/m";
|
||||
}
|
||||
}
|
||||
58
app/Console/Commands/MakeThumbnails.php
Normal file
58
app/Console/Commands/MakeThumbnails.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Image;
|
||||
use App\Services\ImageService;
|
||||
use Illuminate\Console\Command;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
class MakeThumbnails extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'lsky:thumbnails';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Make images thumbnails.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$progress = new ProgressBar($this->output, Image::query()->count());
|
||||
$progress->setMessage('获取图片处理中...');
|
||||
$progress->start();
|
||||
|
||||
$service = new ImageService();
|
||||
|
||||
/** @var Image $image */
|
||||
foreach (Image::query()->whereNotNull('strategy_id')->cursor() as $image) {
|
||||
try {
|
||||
$service->makeThumbnail(
|
||||
image: $image,
|
||||
data: $image->filesystem()->read($image->pathname),
|
||||
force: true,
|
||||
);
|
||||
$progress->advance();
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("缩略图生成失败, {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
45
app/Console/Commands/Upgrade.php
Normal file
45
app/Console/Commands/Upgrade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Services\UpgradeService;
|
||||
use App\Utils;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Upgrade extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'lsky:upgrade';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Upgrade Lsky Pro.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
return (new UpgradeService(Utils::config(ConfigKey::AppVersion)))->upgrade() ? 0 : 1;
|
||||
}
|
||||
}
|
||||
32
app/Console/Kernel.php
Normal file
32
app/Console/Kernel.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
48
app/Enums/ConfigKey.php
Normal file
48
app/Enums/ConfigKey.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class ConfigKey
|
||||
{
|
||||
/** @var string 是否启用注册 */
|
||||
const IsEnableRegistration = 'is_enable_registration';
|
||||
|
||||
/** @var string 是否启用画廊 */
|
||||
const IsEnableGallery = 'is_enable_gallery';
|
||||
|
||||
/** @var string 是否启用接口 */
|
||||
const IsEnableApi = 'is_enable_api';
|
||||
|
||||
/** @var string 程序名称 */
|
||||
const AppName = 'app_name';
|
||||
|
||||
/** @var string 程序版本 */
|
||||
const AppVersion = 'app_version';
|
||||
|
||||
/** @var string 站点关键字 */
|
||||
const SiteKeywords = 'site_keywords';
|
||||
|
||||
/** @var string 站点描述 */
|
||||
const SiteDescription = 'site_description';
|
||||
|
||||
/** @var string 站点公告 */
|
||||
const SiteNotice = 'site_notice';
|
||||
|
||||
/** @var string icp备案号 */
|
||||
const IcpNo = 'icp_no';
|
||||
|
||||
/** @var string 是否允许游客上传 */
|
||||
const IsAllowGuestUpload = 'is_allow_guest_upload';
|
||||
|
||||
/** @var string 用户初始容量(kb) */
|
||||
const UserInitialCapacity = 'user_initial_capacity';
|
||||
|
||||
/** @var string 账户是否需要验证 */
|
||||
const IsUserNeedVerify = 'is_user_need_verify';
|
||||
|
||||
/** @var string 邮件配置 */
|
||||
const Mail = 'mail';
|
||||
|
||||
/** @var string 角色组默认配置 */
|
||||
const Group = 'group';
|
||||
}
|
||||
63
app/Enums/GroupConfigKey.php
Normal file
63
app/Enums/GroupConfigKey.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class GroupConfigKey
|
||||
{
|
||||
/** @var string 最大文件大小 */
|
||||
const MaximumFileSize = 'maximum_file_size';
|
||||
|
||||
/** @var string 并发上传数量 */
|
||||
const ConcurrentUploadNum = 'concurrent_upload_num';
|
||||
|
||||
/** @var string 上传是否需要审查 */
|
||||
const IsEnableScan = 'is_enable_scan';
|
||||
|
||||
/** @var string 审查配置 */
|
||||
const ScanConfigs = 'scan_configs';
|
||||
|
||||
/** @var string 违规图片审查后动作 */
|
||||
const ScannedAction = 'scanned_action';
|
||||
|
||||
/** @var string 是否启用原图保护功能 */
|
||||
const IsEnableOriginalProtection = 'is_enable_original_protection';
|
||||
|
||||
/** @var string 上传是否启用水印功能 */
|
||||
const IsEnableWatermark = 'is_enable_watermark';
|
||||
|
||||
/** @var string 水印配置 */
|
||||
const WatermarkConfigs = 'watermark_configs';
|
||||
|
||||
/** @var string 每分钟上传限制 */
|
||||
const LimitPerMinute = 'limit_per_minute';
|
||||
|
||||
/** @var string 每小时上传限制 */
|
||||
const LimitPerHour = 'limit_per_hour';
|
||||
|
||||
/** @var string 每天上传数量限制 */
|
||||
const LimitPerDay = 'limit_per_day';
|
||||
|
||||
/** @var string 每周上传数量限制 */
|
||||
const LimitPerWeek = 'limit_per_week';
|
||||
|
||||
/** @var string 每月上传数量限制 */
|
||||
const LimitPerMonth = 'limit_per_month';
|
||||
|
||||
/** @var string 允许上传的文件后缀 */
|
||||
const AcceptedFileSuffixes = 'accepted_file_suffixes';
|
||||
|
||||
/** @var string 路径命名规则 */
|
||||
const PathNamingRule = 'path_naming_rule';
|
||||
|
||||
/** @var string 文件命名规则 */
|
||||
const FileNamingRule = 'file_naming_rule';
|
||||
|
||||
/** @var string 图片缓存时间 */
|
||||
const ImageCacheTtl = 'image_cache_ttl';
|
||||
|
||||
/** @var string 图片保存格式 */
|
||||
const ImageSaveFormat = 'image_save_format';
|
||||
|
||||
/** @var string 图片保存质量 */
|
||||
const ImageSaveQuality = 'image_save_quality';
|
||||
}
|
||||
9
app/Enums/ImagePermission.php
Normal file
9
app/Enums/ImagePermission.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class ImagePermission
|
||||
{
|
||||
const Public = 1; // 公开
|
||||
const Private = 0; // 私有
|
||||
}
|
||||
32
app/Enums/Mail/SmtpOption.php
Normal file
32
app/Enums/Mail/SmtpOption.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Mail;
|
||||
|
||||
final class SmtpOption
|
||||
{
|
||||
const Transport = 'smtp';
|
||||
|
||||
/** @var string 主机地址 */
|
||||
const Host = 'host';
|
||||
|
||||
/** @var string 连接端口 */
|
||||
const Port = 'port';
|
||||
|
||||
/** @var string 加密方式 */
|
||||
const Encryption = 'encryption';
|
||||
|
||||
/** @var string 用户名 */
|
||||
const Username = 'username';
|
||||
|
||||
/** @var string 密码 */
|
||||
const Password = 'password';
|
||||
|
||||
/** @var string 超时时间 */
|
||||
const Timeout = 'timeout';
|
||||
|
||||
/** @var string 发件人地址 */
|
||||
const FromAddress = 'from_address';
|
||||
|
||||
/** @var string 发件人名称 */
|
||||
const FromName = 'from_name';
|
||||
}
|
||||
9
app/Enums/PastedAction.php
Normal file
9
app/Enums/PastedAction.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class PastedAction
|
||||
{
|
||||
const Upload = 2; // 直接上传
|
||||
const Waiting = 1; // 等待上传
|
||||
}
|
||||
21
app/Enums/Scan/AliyunOption.php
Normal file
21
app/Enums/Scan/AliyunOption.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Scan;
|
||||
|
||||
final class AliyunOption
|
||||
{
|
||||
/** @var string AccessKeyId */
|
||||
const AccessKeyId = 'access_key_id';
|
||||
|
||||
/** @var string AccessKeySecret */
|
||||
const AccessKeySecret = 'access_key_secret';
|
||||
|
||||
/** @var string RegionId */
|
||||
const RegionId = 'region_id';
|
||||
|
||||
/** @var string 场景 */
|
||||
const Scenes = 'scenes';
|
||||
|
||||
/** @var string 业务场景 */
|
||||
const BizType = 'biz_type';
|
||||
}
|
||||
15
app/Enums/Scan/NsfwJsOption.php
Normal file
15
app/Enums/Scan/NsfwJsOption.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Scan;
|
||||
|
||||
final class NsfwJsOption
|
||||
{
|
||||
/** @var string 接口地址 */
|
||||
const ApiUrl = 'api_url';
|
||||
|
||||
/** @var string 表单属性名称 */
|
||||
const AttrName = 'attr_name';
|
||||
|
||||
/** @var string 阈值 */
|
||||
const Threshold = 'threshold';
|
||||
}
|
||||
21
app/Enums/Scan/TencentOption.php
Normal file
21
app/Enums/Scan/TencentOption.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Scan;
|
||||
|
||||
final class TencentOption
|
||||
{
|
||||
/** @var string SecretId */
|
||||
const SecretId = 'secret_id';
|
||||
|
||||
/** @var string SecretKey */
|
||||
const SecretKey = 'secret_key';
|
||||
|
||||
/** @var string Region */
|
||||
const Region = 'region';
|
||||
|
||||
/** @var string Endpoint */
|
||||
const Endpoint = 'endpoint';
|
||||
|
||||
/** @var string 业务场景 */
|
||||
const BizType = 'biz_type';
|
||||
}
|
||||
24
app/Enums/Strategy/CosOption.php
Normal file
24
app/Enums/Strategy/CosOption.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class CosOption
|
||||
{
|
||||
/** @var string 访问地址 */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string AppId */
|
||||
const AppId = 'app_id';
|
||||
|
||||
/** @var string SecretId */
|
||||
const SecretId = 'secret_id';
|
||||
|
||||
/** @var string SecretKey */
|
||||
const SecretKey = 'secret_key';
|
||||
|
||||
/** @var string 所属地域 */
|
||||
const Region = 'region';
|
||||
|
||||
/** @var string 储存桶名称 */
|
||||
const Bucket = 'bucket';
|
||||
}
|
||||
30
app/Enums/Strategy/FtpOption.php
Normal file
30
app/Enums/Strategy/FtpOption.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class FtpOption
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string 根目录 */
|
||||
const Root = 'root';
|
||||
|
||||
/** @var string 主机地址 */
|
||||
const Host = 'host';
|
||||
|
||||
/** @var string 连接名称 */
|
||||
const Username = 'username';
|
||||
|
||||
/** @var string 密码 */
|
||||
const Password = 'password';
|
||||
|
||||
/** @var string 连接端口 */
|
||||
const Port = 'port';
|
||||
|
||||
/** @var string 加密连接 */
|
||||
const Ssl = 'ssl';
|
||||
|
||||
/** @var string 被动模式 */
|
||||
const Passive = 'passive';
|
||||
}
|
||||
18
app/Enums/Strategy/KodoOption.php
Normal file
18
app/Enums/Strategy/KodoOption.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class KodoOption
|
||||
{
|
||||
/** @var string 访问地址 */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string AccessKey */
|
||||
const AccessKey = 'access_key';
|
||||
|
||||
/** @var string SecretKey */
|
||||
const SecretKey = 'secret_key';
|
||||
|
||||
/** @var string Bucket */
|
||||
const Bucket = 'bucket';
|
||||
}
|
||||
12
app/Enums/Strategy/LocalOption.php
Normal file
12
app/Enums/Strategy/LocalOption.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class LocalOption
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string 根目录 */
|
||||
const Root = 'root';
|
||||
}
|
||||
27
app/Enums/Strategy/MinioOption.php
Normal file
27
app/Enums/Strategy/MinioOption.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class MinioOption
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string AccessKey */
|
||||
const AccessKey = 'access_key';
|
||||
|
||||
/** @var string SecretKey */
|
||||
const SecretKey = 'secret_key';
|
||||
|
||||
/** @var string Endpoint */
|
||||
const Endpoint = 'endpoint';
|
||||
|
||||
/** @var string 区域 */
|
||||
const Region = 'region';
|
||||
|
||||
/** @var string Bucket */
|
||||
const Bucket = 'bucket';
|
||||
|
||||
/** @var string BucketEndpoint */
|
||||
const BucketEndpoint = 'bucket_endpoint';
|
||||
}
|
||||
21
app/Enums/Strategy/OssOption.php
Normal file
21
app/Enums/Strategy/OssOption.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class OssOption
|
||||
{
|
||||
/** @var string 访问地址 */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string AccessKeyId */
|
||||
const AccessKeyId = 'access_key_id';
|
||||
|
||||
/** @var string AccessKeySecret */
|
||||
const AccessKeySecret = 'access_key_secret';
|
||||
|
||||
/** @var string Endpoint */
|
||||
const Endpoint = 'endpoint';
|
||||
|
||||
/** @var string 储存桶名称 */
|
||||
const Bucket = 'bucket';
|
||||
}
|
||||
24
app/Enums/Strategy/S3Option.php
Normal file
24
app/Enums/Strategy/S3Option.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class S3Option
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string AccessKeyId */
|
||||
const AccessKeyId = 'access_key_id';
|
||||
|
||||
/** @var string SecretAccessKey */
|
||||
const SecretAccessKey = 'secret_access_key';
|
||||
|
||||
/** @var string Endpoint */
|
||||
const Endpoint = 'endpoint';
|
||||
|
||||
/** @var string 区域 */
|
||||
const Region = 'region';
|
||||
|
||||
/** @var string Bucket */
|
||||
const Bucket = 'bucket';
|
||||
}
|
||||
33
app/Enums/Strategy/SftpOption.php
Normal file
33
app/Enums/Strategy/SftpOption.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class SftpOption
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string 根目录 */
|
||||
const Root = 'root';
|
||||
|
||||
/** @var string 主机地址 */
|
||||
const Host = 'host';
|
||||
|
||||
/** @var string 连接名称 */
|
||||
const Username = 'username';
|
||||
|
||||
/** @var string 密码 */
|
||||
const Password = 'password';
|
||||
|
||||
/** @var string 私钥 */
|
||||
const PrivateKey = 'private_key';
|
||||
|
||||
/** @var string 密钥口令 */
|
||||
const Passphrase = 'passphrase';
|
||||
|
||||
/** @var string 连接端口 */
|
||||
const Port = 'port';
|
||||
|
||||
/** @var string 使用代理 */
|
||||
const UseAgent = 'use_agent';
|
||||
}
|
||||
18
app/Enums/Strategy/UssOption.php
Normal file
18
app/Enums/Strategy/UssOption.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class UssOption
|
||||
{
|
||||
/** @var string 访问地址 */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string 操作员 */
|
||||
const Operator = 'operator';
|
||||
|
||||
/** @var string 密码 */
|
||||
const Password = 'password';
|
||||
|
||||
/** @var string 服务名称 */
|
||||
const Service = 'service';
|
||||
}
|
||||
24
app/Enums/Strategy/WebDavOption.php
Normal file
24
app/Enums/Strategy/WebDavOption.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Strategy;
|
||||
|
||||
final class WebDavOption
|
||||
{
|
||||
/** @var string 访问url */
|
||||
const Url = 'url';
|
||||
|
||||
/** @var string baseUri */
|
||||
const BaseUri = 'base_uri';
|
||||
|
||||
/** @var string 用户名 */
|
||||
const Username = 'username';
|
||||
|
||||
/** @var string 密码 */
|
||||
const Password = 'password';
|
||||
|
||||
/** @var string 认证方式 */
|
||||
const AuthType = 'auth_type';
|
||||
|
||||
/** @var string 地址前缀 */
|
||||
const Prefix = 'prefix';
|
||||
}
|
||||
36
app/Enums/StrategyKey.php
Normal file
36
app/Enums/StrategyKey.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class StrategyKey
|
||||
{
|
||||
/** @var int 本地 */
|
||||
const Local = 1;
|
||||
|
||||
/** @var int S3 */
|
||||
const S3 = 2;
|
||||
|
||||
/** @var int 阿里云 Oss */
|
||||
const Oss = 3;
|
||||
|
||||
/** @var int 腾讯云 Cos */
|
||||
const Cos = 4;
|
||||
|
||||
/** @var int 七牛云 Kodo */
|
||||
const Kodo = 5;
|
||||
|
||||
/** @var int 又拍云 Uss */
|
||||
const Uss = 6;
|
||||
|
||||
/** @var int Sftp */
|
||||
const Sftp = 7;
|
||||
|
||||
/** @var int Ftp */
|
||||
const Ftp = 8;
|
||||
|
||||
/** @var int WebDav */
|
||||
const Webdav = 9;
|
||||
|
||||
/** @var int Minio */
|
||||
const Minio = 10;
|
||||
}
|
||||
21
app/Enums/UserConfigKey.php
Normal file
21
app/Enums/UserConfigKey.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class UserConfigKey
|
||||
{
|
||||
/** @var string 默认相册 */
|
||||
const DefaultAlbum = 'default_album';
|
||||
|
||||
/** @var string 默认策略 */
|
||||
const DefaultStrategy = 'default_strategy';
|
||||
|
||||
/** @var string 默认权限 */
|
||||
const DefaultPermission = 'default_permission';
|
||||
|
||||
/** @var string 图片粘贴后动作 */
|
||||
const PastedAction = 'pasted_action';
|
||||
|
||||
/** @var string 上传是否自动清除预览 */
|
||||
const IsAutoClearPreview = 'is_auto_clear_preview';
|
||||
}
|
||||
12
app/Enums/UserStatus.php
Normal file
12
app/Enums/UserStatus.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class UserStatus
|
||||
{
|
||||
/** @var int 正常 */
|
||||
const Normal = 1;
|
||||
|
||||
/** @var int 冻结 */
|
||||
const Frozen = 0;
|
||||
}
|
||||
30
app/Enums/Watermark/FontOption.php
Normal file
30
app/Enums/Watermark/FontOption.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Watermark;
|
||||
|
||||
final class FontOption
|
||||
{
|
||||
/** @var string 文本内容 */
|
||||
const Text = 'text';
|
||||
|
||||
/** @var string 文字颜色 */
|
||||
const Color = 'color';
|
||||
|
||||
/** @var string 字体大小 */
|
||||
const Size = 'size';
|
||||
|
||||
/** @var string 字体文件路径 */
|
||||
const Font = 'font';
|
||||
|
||||
/** @var string 水印位置 */
|
||||
const Position = 'position';
|
||||
|
||||
/** @var string 旋转角度 */
|
||||
const Angle = 'angle';
|
||||
|
||||
/** @var string X轴偏移量 */
|
||||
const X = 'x';
|
||||
|
||||
/** @var string Y轴偏移量 */
|
||||
const Y = 'y';
|
||||
}
|
||||
30
app/Enums/Watermark/ImageOption.php
Normal file
30
app/Enums/Watermark/ImageOption.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Watermark;
|
||||
|
||||
final class ImageOption
|
||||
{
|
||||
/** @var string 水印图片 */
|
||||
const Image = 'image';
|
||||
|
||||
/** @var string 水印位置 */
|
||||
const Position = 'position';
|
||||
|
||||
/** @var string 水印图片宽 */
|
||||
const Width = 'width';
|
||||
|
||||
/** @var string 水印图片高 */
|
||||
const Height = 'height';
|
||||
|
||||
/** @var string 旋转角度 */
|
||||
const Rotate = 'rotate';
|
||||
|
||||
/** @var string 不透明度 */
|
||||
const Opacity = 'opacity';
|
||||
|
||||
/** @var string X轴偏移量 */
|
||||
const X = 'x';
|
||||
|
||||
/** @var string Y轴偏移量 */
|
||||
const Y = 'y';
|
||||
}
|
||||
9
app/Enums/Watermark/Mode.php
Normal file
9
app/Enums/Watermark/Mode.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Watermark;
|
||||
|
||||
final class Mode
|
||||
{
|
||||
const Overlay = 1; // 覆盖原图
|
||||
const Dynamic = 2; // 动态生成
|
||||
}
|
||||
57
app/Exceptions/Handler.php
Normal file
57
app/Exceptions/Handler.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Http\Result;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
use Result;
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
|
||||
$this->renderable(function (ThrottleRequestsException $e) {
|
||||
return $this->fail($e->getMessage())->setStatusCode(429);
|
||||
});
|
||||
}
|
||||
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
return $this->shouldReturnJson($request, $exception)
|
||||
? $this->fail($exception->getMessage())->setStatusCode(401)
|
||||
: redirect()->guest($exception->redirectTo() ?? route('login'));
|
||||
}
|
||||
}
|
||||
9
app/Exceptions/UploadException.php
Normal file
9
app/Exceptions/UploadException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UploadException extends Exception
|
||||
{
|
||||
}
|
||||
65
app/Http/Controllers/Admin/ConsoleController.php
Normal file
65
app/Http/Controllers/Admin/ConsoleController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Image;
|
||||
use App\Models\User;
|
||||
use App\Utils;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConsoleController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$carbon = Carbon::now();
|
||||
$format = 'Y-m-d H:i:s';
|
||||
|
||||
$numbers = [
|
||||
'today' => Image::query()->whereBetween('created_at', [$carbon->startOfDay()->format($format), $carbon->endOfDay()->format($format)])->count(),
|
||||
'yesterday' => Image::query()->whereBetween('created_at', [$carbon->yesterday()->startOfDay()->format($format), $carbon->yesterday()->endOfDay()->format($format)])->count(),
|
||||
'week' => Image::query()->whereBetween('created_at', [$carbon->startOfWeek()->format($format), $carbon->endOfWeek()->format($format)])->count(),
|
||||
'month' => Image::query()->whereBetween('created_at', [$carbon->startOfMonth()->format($format), $carbon->endOfMonth()->format($format)])->count(),
|
||||
];
|
||||
|
||||
$start = Carbon::now()->parse('-30 day')->startOfDay();
|
||||
$end = Carbon::now()->endOfDay();
|
||||
$dates = Utils::makeDateRange($start->format('Y-m-d'), $end->format('Y-m-d'));
|
||||
|
||||
$fields = ['游客上传', '用户上传', '新用户'];
|
||||
|
||||
$images = Image::query()
|
||||
->whereBetween('created_at', [$start->format($format), $end->format($format)])
|
||||
->get(['user_id', 'created_at'])
|
||||
->transform(function (Image $image) {
|
||||
$image['date'] = $image->created_at->format('Y-m-d');
|
||||
return $image;
|
||||
})->groupBy('date');
|
||||
|
||||
$users = User::query()
|
||||
->whereBetween('created_at', [$start->format($format), $end->format($format)])
|
||||
->get()
|
||||
->transform(function (User $user) {
|
||||
$user['date'] = $user->created_at->format('Y-m-d');
|
||||
return $user;
|
||||
})->groupBy('date');
|
||||
|
||||
$data = collect(array_map(fn() => 0, array_flip($dates)));
|
||||
$array = [
|
||||
$data->merge($images->map(fn(Collection $items) => $items->whereNull('user_id')->count())),
|
||||
$data->merge($images->map(fn(Collection $items) => $items->whereNotNull('user_id')->count())),
|
||||
$data->merge($users->map(fn(Collection $items) => $items->count())),
|
||||
];
|
||||
$datasets = collect($fields)->transform(function ($item, $index) use ($dates, $array) {
|
||||
return [
|
||||
'name' => $item,
|
||||
'type' => 'line',
|
||||
'data' => $array[$index]->values(),
|
||||
];
|
||||
});
|
||||
|
||||
return view('admin.console.index', compact('fields', 'numbers', 'dates', 'datasets'));
|
||||
}
|
||||
}
|
||||
125
app/Http/Controllers/Admin/GroupController.php
Normal file
125
app/Http/Controllers/Admin/GroupController.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\GroupRequest;
|
||||
use App\Models\Group;
|
||||
use App\Models\Image;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GroupController extends Controller
|
||||
{
|
||||
private function share()
|
||||
{
|
||||
\Illuminate\Support\Facades\View::share([
|
||||
'default' => Group::getDefaultConfigs(),
|
||||
'positions' => Group::POSITIONS,
|
||||
'scenes' => Group::SCENES,
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$keywords = $request->query('keywords');
|
||||
$groups = Group::query()->when($keywords, function (Builder $builder, $keywords) {
|
||||
$builder->where('name', 'like', "%{$keywords}%");
|
||||
})->withCount('users')->withCount('strategies')->latest()->paginate();
|
||||
|
||||
$groups->appends(compact('keywords'));
|
||||
|
||||
$this->share();
|
||||
|
||||
return view('admin.group.index', compact('groups'));
|
||||
}
|
||||
|
||||
public function add(): View
|
||||
{
|
||||
$this->share();
|
||||
|
||||
return view('admin.group.add');
|
||||
}
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$group = Group::query()->findOrFail($request->route('id'));
|
||||
|
||||
$this->share();
|
||||
|
||||
return view('admin.group.edit', compact('group'));
|
||||
}
|
||||
|
||||
public function create(GroupRequest $request): Response
|
||||
{
|
||||
DB::transaction(function () use ($request) {
|
||||
$group = new Group();
|
||||
$group->fill($request->validated());
|
||||
$group->save();
|
||||
});
|
||||
|
||||
$this->share();
|
||||
|
||||
return $this->success('创建成功');
|
||||
}
|
||||
|
||||
public function update(GroupRequest $request): Response
|
||||
{
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
/** @var Group $group */
|
||||
$group = Group::query()->findOrFail($request->route('id'));
|
||||
$group->fill($request->validated());
|
||||
if ($group->isDirty('is_default') && ! $group->is_default) {
|
||||
if (! Group::query()->where('is_default', true)->where('id', '<>', $group->id)->exists()) {
|
||||
return $this->fail('系统至少需要保留一个默认组');
|
||||
}
|
||||
}
|
||||
if ($group->isDirty('is_guest') && ! $group->is_guest) {
|
||||
if (! Group::query()->where('is_guest', true)->where('id', '<>', $group->id)->exists()) {
|
||||
return $this->fail('系统至少需要保留一个游客组');
|
||||
}
|
||||
}
|
||||
$group->save();
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return $this->fail('保存失败');
|
||||
}
|
||||
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var Group $group */
|
||||
if ($group = Group::query()->find($request->route('id'))) {
|
||||
if ($group->is_default || $group->is_guest) {
|
||||
return $this->fail('默认组和游客组无法删除');
|
||||
}
|
||||
DB::transaction(function () use ($group) {
|
||||
$group->users()->update(['group_id' => Group::query()->where('is_default', true)->value('id')]);
|
||||
$group->delete();
|
||||
});
|
||||
}
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
|
||||
public function clearCache(Request $request): Response
|
||||
{
|
||||
/** @var Group $group */
|
||||
if ($group = Group::query()->find($request->route('id'))) {
|
||||
/** @var Image $image */
|
||||
foreach ($group->images()->select('key')->cursor() as $image) {
|
||||
Cache::forget('image_'.$image->key);
|
||||
}
|
||||
}
|
||||
return $this->success('清除成功');
|
||||
}
|
||||
}
|
||||
96
app/Http/Controllers/Admin/ImageController.php
Normal file
96
app/Http/Controllers/Admin/ImageController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\ImagePermission;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Image;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$keywords = $request->query('keywords');
|
||||
$images = Image::query()->with(['user' => function (BelongsTo $belongsTo) {
|
||||
$belongsTo->withSum('images', 'size');
|
||||
}, 'album', 'group', 'strategy'])->when($keywords, function (Builder $builder, $keywords) {
|
||||
$words = [];
|
||||
$qualifiers = [
|
||||
'name:', 'album:', 'group:', 'strategy:', 'email:', 'extension:', 'md5:', 'sha1:', 'ip:', 'is:', 'order:',
|
||||
];
|
||||
collect(array_filter(explode(' ', $keywords)))->filter(function ($keyword) use ($qualifiers, &$words) {
|
||||
if (Str::startsWith($keyword, $qualifiers)) {
|
||||
return true;
|
||||
}
|
||||
$words[] = $keyword;
|
||||
return false;
|
||||
})->each(function ($filter) use ($builder) {
|
||||
match ($filter) {
|
||||
'is:public' => $builder->where('permission', ImagePermission::Public),
|
||||
'is:private' => $builder->where('permission', ImagePermission::Private),
|
||||
'is:unhealthy' => $builder->where('is_unhealthy', 1),
|
||||
'is:guest' => $builder->whereNull('user_id'),
|
||||
'is:adminer' => $builder->whereHas('user', fn (Builder $builder) => $builder->where('is_adminer', 1)),
|
||||
'order:earliest' => $builder->orderBy('created_at'),
|
||||
'order:utmost' => $builder->orderByDesc('size'),
|
||||
'order:least' => $builder->orderBy('size'),
|
||||
default => 0,
|
||||
};
|
||||
|
||||
[$qualifier, $value] = explode(':', $filter);
|
||||
|
||||
if ($value) {
|
||||
$callback = fn (Builder $builder) => $builder->where('name', $value);
|
||||
match ($qualifier) {
|
||||
'name' => $builder->whereHas('user', $callback),
|
||||
'album' => $builder->whereHas('album', $callback),
|
||||
'group' => $builder->whereHas('group', $callback),
|
||||
'strategy' => $builder->whereHas('strategy', $callback),
|
||||
'email' => $builder->whereHas('user', fn (Builder $builder) => $builder->where('email', $value)),
|
||||
'extension' => $builder->where('extension', $value),
|
||||
'md5' => $builder->where('md5', $value),
|
||||
'sha1' => $builder->where('sha1', $value),
|
||||
'ip' => $builder->where('ip', $value),
|
||||
default => 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
foreach ($words as $word) {
|
||||
$builder->where('name', 'like', "%{$word}%")
|
||||
->orWhere('origin_name', 'like', "%{$word}%")
|
||||
->orWhere('alias_name', 'like', "%{$word}%");
|
||||
}
|
||||
})->latest()->paginate(40);
|
||||
$images->getCollection()->each(function (Image $image) {
|
||||
$image->append('url', 'pathname', 'thumb_url');
|
||||
$image->album?->setVisible(['name']);
|
||||
$image->group?->setVisible(['name']);
|
||||
$image->strategy?->setVisible(['name']);
|
||||
});
|
||||
|
||||
$images->appends(compact('keywords'));
|
||||
|
||||
return view('admin.image.index', compact('images'));
|
||||
}
|
||||
|
||||
public function update(): Response
|
||||
{
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var Image $image */
|
||||
$image = Image::with('user', 'strategy', 'album')->find($request->route('id'));
|
||||
(new UserService())->deleteImages([$image->id]);
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
78
app/Http/Controllers/Admin/SettingController.php
Normal file
78
app/Http/Controllers/Admin/SettingController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\Test;
|
||||
use App\Models\Config;
|
||||
use App\Services\UpgradeService;
|
||||
use App\Utils;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$configs = Utils::config();
|
||||
return view('admin.setting.index', compact('configs'));
|
||||
}
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
foreach ($request->all() as $key => $value) {
|
||||
Config::query()->where('name', $key)->update(['value' => $value]);
|
||||
}
|
||||
Cache::flush();
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
public function mailTest(Request $request): Response
|
||||
{
|
||||
try {
|
||||
Mail::to($request->post('email'))->send(new Test());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
return $this->success('发送成功');
|
||||
}
|
||||
|
||||
public function checkUpdate(): Response
|
||||
{
|
||||
$version = Utils::config(ConfigKey::AppVersion);
|
||||
$service = new UpgradeService($version);
|
||||
try {
|
||||
$data = [
|
||||
'is_update' => $service->check(),
|
||||
];
|
||||
if ($data['is_update']) {
|
||||
$data['version'] = $service->getVersions()->first();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success('success', $data);
|
||||
}
|
||||
|
||||
public function upgrade()
|
||||
{
|
||||
ignore_user_abort(true);
|
||||
set_time_limit(0);
|
||||
|
||||
$version = Utils::config(ConfigKey::AppVersion);
|
||||
$service = new UpgradeService($version);
|
||||
$this->success()->send();
|
||||
$service->upgrade();
|
||||
flush();
|
||||
}
|
||||
|
||||
public function upgradeProgress(): Response
|
||||
{
|
||||
return $this->success('success', Cache::get('upgrade_progress'));
|
||||
}
|
||||
}
|
||||
75
app/Http/Controllers/Admin/StrategyController.php
Normal file
75
app/Http/Controllers/Admin/StrategyController.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\StrategyRequest;
|
||||
use App\Models\Strategy;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StrategyController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$keywords = $request->query('keywords');
|
||||
$strategies = Strategy::query()->when($keywords, function (Builder $builder, $keywords) {
|
||||
$builder->where('name', 'like', "%{$keywords}%")->orWhere('intro', 'like', "%{$keywords}%");
|
||||
})->withCount('images')->withSum('images', 'size')->latest()->paginate();
|
||||
|
||||
$strategies->appends(compact('keywords'));
|
||||
|
||||
return view('admin.strategy.index', compact('strategies'));
|
||||
}
|
||||
|
||||
public function add(): View
|
||||
{
|
||||
return view('admin.strategy.add');
|
||||
}
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
/** @var Strategy $strategy */
|
||||
$strategy = Strategy::query()->findOrFail($request->route('id'));
|
||||
return view('admin.strategy.edit', compact('strategy'));
|
||||
}
|
||||
|
||||
public function create(StrategyRequest $request): Response
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$strategy = new Strategy($validated);
|
||||
DB::transaction(function () use ($strategy, $validated) {
|
||||
$strategy->save();
|
||||
$strategy->groups()->attach($validated['groups'] ?? []);
|
||||
});
|
||||
return $this->success('创建成功');
|
||||
}
|
||||
|
||||
public function update(StrategyRequest $request): Response
|
||||
{
|
||||
$validated = $request->validated();
|
||||
/** @var Strategy $strategy */
|
||||
$strategy = Strategy::query()->findOrFail($request->route('id'));
|
||||
$strategy->fill($request->validated());
|
||||
DB::transaction(function () use ($strategy, $validated) {
|
||||
$strategy->save();
|
||||
$strategy->groups()->sync($validated['groups'] ?? []);
|
||||
});
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var Strategy $strategy */
|
||||
if ($strategy = Strategy::query()->find($request->route('id'))) {
|
||||
DB::transaction(function () use ($strategy) {
|
||||
$strategy->images()->update(['strategy_id' => null]);
|
||||
$strategy->delete();
|
||||
});
|
||||
}
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
81
app/Http/Controllers/Admin/UserController.php
Normal file
81
app/Http/Controllers/Admin/UserController.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UserRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$status = $request->query('status');
|
||||
$keywords = $request->query('keywords');
|
||||
$users = User::query()->when($status > -1, function (Builder $builder) use ($status) {
|
||||
$builder->where('status', $status);
|
||||
})->when($keywords, function (Builder $builder, $keywords) {
|
||||
$builder->where('name', 'like', "%{$keywords}%")->orWhere('email', 'like', "%{$keywords}%");
|
||||
})->with('group')->withSum('images', 'size')->latest()->paginate();
|
||||
$users->getCollection()->each(function (User $user) {
|
||||
$user->group->setVisible(['name']);
|
||||
});
|
||||
$statuses = [-1 => '全部', 1 => '正常', 0 => '冻结'];
|
||||
|
||||
$users->appends(compact('status', 'keywords'));
|
||||
|
||||
return view('admin.user.index', compact('users', 'statuses'));
|
||||
}
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$user = User::query()->findOrFail($request->route('id'));
|
||||
return view('admin.user.edit', compact('user'));
|
||||
}
|
||||
|
||||
public function update(UserRequest $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::query()->findOrFail($request->route('id'));
|
||||
$validated = $request->validated();
|
||||
|
||||
if (! empty($validated['password'])) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($validated['password']),
|
||||
'remember_token' => Str::random(60),
|
||||
]);
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
|
||||
unset($validated['password']);
|
||||
$user->fill($validated);
|
||||
$user->group_id = $validated['group_id'];
|
||||
|
||||
if (!$user->save()) {
|
||||
return $this->fail('保存失败');
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
if ($user = User::query()->find($request->route('id'))) {
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->images()->update(['user_id' => null]);
|
||||
$user->albums()->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
32
app/Http/Controllers/Api/V1/AlbumController.php
Normal file
32
app/Http/Controllers/Api/V1/AlbumController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Album;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$albums = $user->albums()->filter($request)->paginate(40);
|
||||
$albums->getCollection()->each(function (Album $album) {
|
||||
$album->setVisible(['id', 'name', 'intro', 'image_num']);
|
||||
});
|
||||
return $this->success('success', $albums);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user->albums()->where('id', $request->route('id'))->delete();
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
83
app/Http/Controllers/Api/V1/ImageController.php
Normal file
83
app/Http/Controllers/Api/V1/ImageController.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\UploadException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Image;
|
||||
use App\Models\User;
|
||||
use App\Services\ImageService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
/**
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function upload(Request $request, ImageService $service): Response
|
||||
{
|
||||
if ($request->hasHeader('Authorization')) {
|
||||
$guards = array_keys(config('auth.guards'));
|
||||
|
||||
if (empty($guards)) {
|
||||
$guards = [null];
|
||||
}
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
Auth::shouldUse($guard);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! Auth::check()) {
|
||||
throw new AuthenticationException('Authentication failed.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$image = $service->store($request);
|
||||
} catch (UploadException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
Utils::e($e, 'Api 上传文件时发生异常');
|
||||
if (config('app.debug')) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
return $this->fail('服务异常,请稍后再试');
|
||||
}
|
||||
return $this->success('上传成功', $image->setAppends(['pathname', 'links'])->only(
|
||||
'key', 'name', 'pathname', 'origin_name', 'size', 'mimetype', 'extension', 'md5', 'sha1', 'links'
|
||||
));
|
||||
}
|
||||
|
||||
public function images(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$images = $user->images()->filter($request)->paginate(40)->withQueryString();
|
||||
$images->getCollection()->each(function (Image $image) {
|
||||
$image->human_date = $image->created_at->diffForHumans();
|
||||
$image->date = $image->created_at->format('Y-m-d H:i:s');
|
||||
$image->append(['pathname', 'links'])->setVisible([
|
||||
'album', 'key', 'name', 'pathname', 'origin_name', 'size', 'mimetype', 'extension', 'md5', 'sha1',
|
||||
'width', 'height', 'links', 'human_date', 'date',
|
||||
]);
|
||||
});
|
||||
return $this->success('success', $images);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
(new UserService())->deleteImages([$request->route('key')], $user, 'key');
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
20
app/Http/Controllers/Api/V1/StrategyController.php
Normal file
20
app/Http/Controllers/Api/V1/StrategyController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Group;
|
||||
use App\Models\Strategy;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class StrategyController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
/** @var Group $group */
|
||||
$group = Auth::check() ? Auth::user()->group : Group::query()->where('is_guest', true)->first();
|
||||
$strategies = $group->strategies()->get()->each(fn (Strategy $strategy) => $strategy->setVisible(['id', 'name']));
|
||||
return $this->success('success', compact('strategies'));
|
||||
}
|
||||
}
|
||||
45
app/Http/Controllers/Api/V1/TokenController.php
Normal file
45
app/Http/Controllers/Api/V1/TokenController.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class TokenController extends Controller
|
||||
{
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return $this->fail($e->validator->errors()->first());
|
||||
}
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = User::query()->where('email', $request->email)->first();
|
||||
|
||||
if (! $user || ! Hash::check($request->password, $user->password)) {
|
||||
return $this->fail('The email address or password is incorrect.');
|
||||
}
|
||||
|
||||
$token = $user->createToken($user->email)->plainTextToken;
|
||||
|
||||
return $this->success('success', compact('token'));
|
||||
}
|
||||
|
||||
public function clear(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user->tokens()->delete();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
22
app/Http/Controllers/Api/V1/UserController.php
Normal file
22
app/Http/Controllers/Api/V1/UserController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user->used_capacity = $user->images()->sum('size') + 0;
|
||||
$user->setVisible([
|
||||
'name', 'avatar', 'email', 'capacity', 'used_capacity', 'url', 'image_num', 'album_num', 'registered_ip'
|
||||
]);
|
||||
return $this->success('success', $user);
|
||||
}
|
||||
}
|
||||
54
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
54
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*
|
||||
* @param \App\Http\Requests\Auth\LoginRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store(LoginRequest $request)
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
44
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
return view('auth.confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(RouteServiceProvider::HOME)
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
65
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
65
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function ($user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
47
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
59
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use App\Utils;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'registered_ip' => $request->ip(),
|
||||
]);
|
||||
|
||||
if (Utils::config(ConfigKey::IsUserNeedVerify)) {
|
||||
event(new Registered($user));
|
||||
}
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
30
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*
|
||||
* @param \Illuminate\Foundation\Auth\EmailVerificationRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request)
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
|
||||
}
|
||||
}
|
||||
14
app/Http/Controllers/Common/ApiController.php
Normal file
14
app/Http/Controllers/Common/ApiController.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Common;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ApiController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('common.api');
|
||||
}
|
||||
}
|
||||
23
app/Http/Controllers/Common/GalleryController.php
Normal file
23
app/Http/Controllers/Common/GalleryController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Common;
|
||||
|
||||
use App\Enums\ImagePermission;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Image;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GalleryController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$images = Image::query()
|
||||
->with('user')
|
||||
->whereNotNull('user_id')
|
||||
->where('is_unhealthy', false)
|
||||
->where('permission', ImagePermission::Public)
|
||||
->latest()
|
||||
->simplePaginate(40);
|
||||
return view('common.gallery', compact('images'));
|
||||
}
|
||||
}
|
||||
205
app/Http/Controllers/Controller.php
Normal file
205
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\GroupConfigKey;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Enums\Watermark\Mode;
|
||||
use App\Exceptions\UploadException;
|
||||
use App\Http\Result;
|
||||
use App\Models\Group;
|
||||
use App\Models\Image;
|
||||
use App\Models\Strategy;
|
||||
use App\Models\User;
|
||||
use App\Services\ImageService;
|
||||
use App\Utils;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
use Intervention\Image\Facades\Image as InterventionImage;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, DispatchesJobs, ValidatesRequests, Result;
|
||||
|
||||
public function install(Request $request): View|Response
|
||||
{
|
||||
if (file_exists(base_path('installed.lock'))) {
|
||||
if ($request->expectsJson()) {
|
||||
return $this->fail('Already installed. if you want to reinstall, please remove installed.lock file.');
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$extensions = collect([
|
||||
['name' => 'BCMath', 'intro' => '数学精度处理拓展'],
|
||||
['name' => 'Ctype', 'intro' => '字符类型检测拓展'],
|
||||
['name' => 'DOM', 'intro' => 'DOM 对象模型,用于处理文档元素'],
|
||||
['name' => 'Fileinfo', 'intro' => '获取文件信息拓展'],
|
||||
['name' => 'JSON', 'intro' => 'JavaScript 对象符号(JSON)'],
|
||||
['name' => 'Mbstring', 'intro' => '多字节字符串处理拓展'],
|
||||
['name' => 'OpenSSL', 'intro' => '加密拓展'],
|
||||
['name' => 'PCRE', 'intro' => '正则表达式拓展'],
|
||||
['name' => 'PDO', 'intro' => '数据库拓展'],
|
||||
['name' => 'Tokenizer', 'intro' => '令牌处理拓展'],
|
||||
['name' => 'XML', 'intro' => 'Xml 解析器'],
|
||||
['name' => 'Imagick', 'intro' => '高性能图片处理拓展'],
|
||||
])->transform(function ($item) {
|
||||
$item['result'] = extension_loaded(strtolower($item['name']));
|
||||
return $item;
|
||||
})->push([
|
||||
'name' => 'readlink、symlink 函数',
|
||||
'intro' => '读取、创建符号链接函数',
|
||||
'result' => function_exists('readlink') && function_exists('symlink'),
|
||||
])->push([
|
||||
'name' => 'putenv、getenv 函数',
|
||||
'intro' => '设置和获取环境变量函数',
|
||||
'result' => function_exists('putenv') && function_exists('getenv'),
|
||||
])->push([
|
||||
'name' => 'exec、shell_exec 函数',
|
||||
'intro' => '执行外部命令',
|
||||
'result' => function_exists('exec') && function_exists('shell_exec'),
|
||||
])->push([
|
||||
'name' => 'chmod、chown、fileperms 函数',
|
||||
'intro' => '设置和获取文件、文件夹权限函数',
|
||||
'result' => function_exists('chmod') && function_exists('chown') && function_exists('fileperms'),
|
||||
])->push([
|
||||
'name' => 'PHP >= 8.0.2',
|
||||
'intro' => '最低要求 PHP 8.0.2 版本',
|
||||
'result' => phpversion() >= 8,
|
||||
]);
|
||||
|
||||
$status = ! $extensions->where('result', false)->isNotEmpty();
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
try {
|
||||
$request->validate([
|
||||
'account.email' => 'required|email',
|
||||
'account.password' => 'required|between:6,32'
|
||||
], [], [
|
||||
'account.email' => '管理员账号邮箱',
|
||||
'account.password' => '管理员账号密码'
|
||||
]);
|
||||
|
||||
$data = collect($request->only([
|
||||
'connection', 'host', 'port', 'database', 'username', 'password',
|
||||
]))->transform(fn($item, $key) => ['--'.$key => $item])->collapse();
|
||||
$output = new BufferedOutput();
|
||||
$exitCode = Artisan::call('lsky:install', $data->toArray(), $output);
|
||||
if ($exitCode) {
|
||||
throw new \Exception(str_replace(PHP_EOL, '<br/>', $output->fetch()));
|
||||
}
|
||||
$user = new User([
|
||||
'name' => '超级管理员',
|
||||
'email' => $request->input('account.email'),
|
||||
'password' => Hash::make($request->input('account.password')),
|
||||
'registered_ip' => $request->ip(),
|
||||
]);
|
||||
$user->group_id = Group::query()->first()['id'];
|
||||
$user->is_adminer = true;
|
||||
$user->status = UserStatus::Normal;
|
||||
$user->email_verified_at = date('Y-m-d H:i:s');
|
||||
// 设置默认策略组的 url 为当前请求 url
|
||||
Strategy::query()->update(['configs->url' => $request->getSchemeAndHttpHost().'/i']);
|
||||
$user->save();
|
||||
} catch (\Throwable $e) {
|
||||
@unlink(base_path('installed.lock'));
|
||||
Utils::e($e, '执行安装程序时出现异常');
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
return view('install', compact('extensions', 'status'));
|
||||
}
|
||||
|
||||
public function upload(Request $request, ImageService $service): Response
|
||||
{
|
||||
try {
|
||||
$image = $service->store($request);
|
||||
} catch (UploadException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
Utils::e($e, 'Web 上传文件时发生异常');
|
||||
if (config('app.debug')) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
return $this->fail('服务异常,请稍后再试');
|
||||
}
|
||||
return $this->success('上传成功', $image->setAppends(['pathname', 'links'])->only(
|
||||
'id', 'pathname', 'origin_name', 'size', 'mimetype', 'md5', 'sha1', 'links'
|
||||
));
|
||||
}
|
||||
|
||||
public function output(Request $request, ImageService $service): StreamedResponse
|
||||
{
|
||||
/** @var Image $image */
|
||||
$image = Image::query()
|
||||
->with('group')
|
||||
->where('key', $request->route('key'))
|
||||
->where('extension', strtolower($request->route('extension')))
|
||||
->firstOr(fn() => abort(404));
|
||||
if (! $image->group?->configs->get(GroupConfigKey::IsEnableOriginalProtection)) {
|
||||
abort(404);
|
||||
}
|
||||
try {
|
||||
$cacheKey = "image_{$image->key}";
|
||||
|
||||
if (Cache::has($cacheKey)) {
|
||||
$contents = Cache::get($cacheKey);
|
||||
} else {
|
||||
$contents = $image->filesystem()->read($image->pathname);
|
||||
$configs = collect($image->group?->configs->get(GroupConfigKey::WatermarkConfigs));
|
||||
|
||||
// 是否启用了水印功能,跳过gif和ico图片
|
||||
if (
|
||||
$image->group?->configs->get(GroupConfigKey::IsEnableWatermark) &&
|
||||
$configs->get('mode', Mode::Overlay) == Mode::Dynamic &&
|
||||
! in_array($image->extension, ['ico', 'gif', 'svg'])
|
||||
) {
|
||||
$quality = $image->group?->configs->get(GroupConfigKey::ImageSaveQuality, 75);
|
||||
$contents = $service->stickWatermark($contents, $configs)->encode($image->extension, $quality)->getEncoded();
|
||||
}
|
||||
$cacheTtl = (int)$image->group?->configs->get(GroupConfigKey::ImageCacheTtl, 0);
|
||||
// 是否启用了缓存
|
||||
if ($cacheTtl) {
|
||||
Cache::remember($cacheKey, $cacheTtl, fn () => $contents);
|
||||
} else {
|
||||
if (Cache::has($cacheKey)) Cache::forget($cacheKey);
|
||||
}
|
||||
}
|
||||
} catch (FilesystemException $e) {
|
||||
Utils::e($e, '图片输出时出现异常');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$mimetype = $image->mimetype;
|
||||
|
||||
// ico svg 图片直接输出,不经过 InterventionImage 处理
|
||||
if (in_array($image->extension, ['ico', 'svg'])) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
// 浏览器无法预览的图片,改为 png 格式输出
|
||||
if (in_array($image->extension, ['psd', 'tif', 'bmp'])) {
|
||||
$mimetype = 'image/png';
|
||||
$contents = InterventionImage::make($contents)->encode('png')->getEncoded();
|
||||
}
|
||||
|
||||
out:
|
||||
|
||||
return \response()->stream(function () use ($contents) {
|
||||
echo $contents;
|
||||
}, headers: ['Content-type' => $mimetype]);
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/User/AlbumController.php
Normal file
69
app/Http/Controllers/User/AlbumController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AlbumRequest;
|
||||
use App\Models\Album;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function albums(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$albums = $user->albums()->latest()->paginate(40);
|
||||
$albums->getCollection()->each(function (Album $album) {
|
||||
$album->setVisible(['id', 'name', 'intro', 'image_num']);
|
||||
});
|
||||
return $this->success('success', compact('albums'));
|
||||
}
|
||||
|
||||
public function create(AlbumRequest $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
DB::transaction(function () use ($user, $request) {
|
||||
$user->albums()->create(array_filter($request->validated()));
|
||||
$user->album_num = $user->albums()->count();
|
||||
$user->save();
|
||||
});
|
||||
|
||||
return $this->success('创建成功');
|
||||
}
|
||||
|
||||
public function update(AlbumRequest $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$album = $user->albums()->find($request->route('id'));
|
||||
if (is_null($album)) {
|
||||
return $this->fail('不存在的相册');
|
||||
}
|
||||
$album->update(array_filter($request->validated()));
|
||||
return $this->success('修改成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
/** @var Album|null $album */
|
||||
$album = $user->albums()->find($request->route('id'));
|
||||
if (is_null($album)) {
|
||||
return $this->fail('不存在的相册');
|
||||
}
|
||||
DB::transaction(function () use ($user, $album) {
|
||||
$album->images()->update(['album_id' => null]);
|
||||
$album->delete();
|
||||
$user->album_num = $user->albums()->count();
|
||||
$user->save();
|
||||
});
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
121
app/Http/Controllers/User/ImageController.php
Normal file
121
app/Http/Controllers/User/ImageController.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Enums\ImagePermission;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ImageRenameRequest;
|
||||
use App\Models\Album;
|
||||
use App\Models\Image;
|
||||
use App\Models\User;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('user.images');
|
||||
}
|
||||
|
||||
public function images(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$images = $user->images()->filter($request)->with('group', 'strategy')->paginate(40);
|
||||
$images->getCollection()->each(function (Image $image) {
|
||||
// 图片宽高过小会导致前端排版异常
|
||||
$image->width = max($image->width, 200);
|
||||
$image->height = max($image->height, 200);
|
||||
|
||||
$image->human_date = $image->created_at->diffForHumans();
|
||||
$image->date = $image->created_at->format('Y-m-d H:i:s');
|
||||
$image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([
|
||||
'id', 'filename', 'url', 'thumb_url', 'human_date', 'date', 'size', 'width', 'height', 'links'
|
||||
]);
|
||||
});
|
||||
return $this->success('success', compact('images'));
|
||||
}
|
||||
|
||||
public function image(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
/** @var Image $image */
|
||||
if (!$image = $user->images()->find($request->route('id'))) {
|
||||
return $this->fail('未找到该图片');
|
||||
}
|
||||
$image->strategy?->setVisible(['name']);
|
||||
$image->album?->setVisible(['name']);
|
||||
$image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([
|
||||
'id', 'filename', 'origin_name', 'url', 'thumb_url', 'width', 'height', 'size', 'mimetype', 'md5', 'sha1',
|
||||
'permission', 'strategy', 'album', 'uploaded_ip', 'links', 'created_at'
|
||||
]);
|
||||
return $this->success('success', compact('image'));
|
||||
}
|
||||
|
||||
public function permission(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$permission = $request->input('permission');
|
||||
$permissions = ['public' => ImagePermission::Public, 'private' => ImagePermission::Private];
|
||||
if (!in_array($permission, array_keys($permissions))) {
|
||||
return $this->fail('设置失败');
|
||||
}
|
||||
$user->images()->whereIn('id', (array) $request->input('ids'))->update([
|
||||
'permission' => $permissions[$permission],
|
||||
]);
|
||||
return $this->success('设置成功');
|
||||
}
|
||||
|
||||
public function rename(ImageRenameRequest $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
/** @var Image $image */
|
||||
if ($image = $user->images()->find($request->input('id'))) {
|
||||
$image->alias_name = $request->input('name');
|
||||
$image->save();
|
||||
}
|
||||
|
||||
return $this->success('重命名成功', $image->only('id', 'filename'));
|
||||
}
|
||||
|
||||
public function movement(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
DB::transaction(function () use ($user, $request) {
|
||||
/** @var null|Album $album */
|
||||
$album = $user->albums()->find((int) $request->input('id'));
|
||||
$user->images()->whereIn('id', $request->input('selected'))->update([
|
||||
'album_id' => $album->id ?? null,
|
||||
]);
|
||||
if ($album) {
|
||||
$album->image_num = $album->images()->count();
|
||||
$album->save();
|
||||
}
|
||||
if ($albumId = (int) $request->input('album_id')) {
|
||||
/** @var Album $originAlbum */
|
||||
$originAlbum = $user->albums()->find($albumId);
|
||||
$originAlbum->image_num = $originAlbum->images()->count();
|
||||
$originAlbum->save();
|
||||
}
|
||||
});
|
||||
return $this->success('移动成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
(new UserService())->deleteImages($request->all() ?: [], $user);
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
}
|
||||
64
app/Http/Controllers/User/UserController.php
Normal file
64
app/Http/Controllers/User/UserController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Enums\UserConfigKey;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UserSettingRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function dashboard(): View
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$configs = $user->group->configs;
|
||||
$strategies = $user->group->strategies()->get();
|
||||
return view('user.dashboard', compact('strategies', 'configs', 'user'));
|
||||
}
|
||||
|
||||
public function settings(): View
|
||||
{
|
||||
return view('user.settings');
|
||||
}
|
||||
|
||||
public function update(UserSettingRequest $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
$user->name = $request->validated('name');
|
||||
$user->url = $request->validated('url') ?: '';
|
||||
$user->configs = $user->configs->merge(collect($request->validated('configs'))->transform(function ($value) {
|
||||
return (int)$value;
|
||||
}));
|
||||
if ($password = $request->validated('password')) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($password),
|
||||
'remember_token' => Str::random(60),
|
||||
]);
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
$user->save();
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
public function setStrategy(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
if (! $strategy = $user->group->strategies()->find($request->id)) {
|
||||
return $this->fail('没有找到该策略');
|
||||
}
|
||||
$user->update(['configs->'.UserConfigKey::DefaultStrategy => $strategy->id]);
|
||||
return $this->success('设置成功');
|
||||
}
|
||||
}
|
||||
68
app/Http/Kernel.php
Normal file
68
app/Http/Kernel.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.admin' => \App\Http\Middleware\AuthenticateWithAdmin::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
}
|
||||
21
app/Http/Middleware/Authenticate.php
Normal file
21
app/Http/Middleware/Authenticate.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function redirectTo($request)
|
||||
{
|
||||
if (! $request->expectsJson()) {
|
||||
return route('login');
|
||||
}
|
||||
}
|
||||
}
|
||||
24
app/Http/Middleware/AuthenticateWithAdmin.php
Normal file
24
app/Http/Middleware/AuthenticateWithAdmin.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthenticateWithAdmin extends Authenticate
|
||||
{
|
||||
public function handle($request, Closure $next, ...$guards)
|
||||
{
|
||||
$this->authenticate($request, $guards);
|
||||
|
||||
/** @var User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user->is_adminer) {
|
||||
return abort(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
app/Http/Middleware/CheckIsEnableApi.php
Normal file
26
app/Http/Middleware/CheckIsEnableApi.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Result;
|
||||
use App\Utils;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckIsEnableApi
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (! Utils::config(ConfigKey::IsEnableApi)) {
|
||||
if ($request->expectsJson()) {
|
||||
return $this->fail('管理员未启用 API')->setStatusCode(403);
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
app/Http/Middleware/CheckIsEnableGallery.php
Normal file
26
app/Http/Middleware/CheckIsEnableGallery.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Result;
|
||||
use App\Utils;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckIsEnableGallery
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (! Utils::config(ConfigKey::IsEnableGallery)) {
|
||||
if ($request->expectsJson()) {
|
||||
return $this->fail('管理员未启用画廊功能')->setStatusCode(403);
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
25
app/Http/Middleware/CheckIsEnableGuestUpload.php
Normal file
25
app/Http/Middleware/CheckIsEnableGuestUpload.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Result;
|
||||
use App\Utils;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CheckIsEnableGuestUpload
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// 检测是否启用了游客上传,未启用、未登录情况下跳转至登录页面
|
||||
if (! Utils::config(ConfigKey::IsAllowGuestUpload) && Auth::guest()) {
|
||||
return redirect('login');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
app/Http/Middleware/CheckIsEnableRegistration.php
Normal file
26
app/Http/Middleware/CheckIsEnableRegistration.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Http\Result;
|
||||
use App\Utils;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckIsEnableRegistration
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (! Utils::config(ConfigKey::IsEnableRegistration)) {
|
||||
if ($request->expectsJson()) {
|
||||
return $this->fail('站点管理员关闭了注册功能')->setStatusCode(403);
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
app/Http/Middleware/CheckIsInstalled.php
Normal file
26
app/Http/Middleware/CheckIsInstalled.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Result;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckIsInstalled
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// 检测程序是否安装
|
||||
if (! file_exists(base_path('installed.lock'))) {
|
||||
if (! $request->expectsJson()) {
|
||||
return redirect('install');
|
||||
} else {
|
||||
return $this->fail('It has already been installed.');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
32
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
32
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
|
||||
* @param string|null ...$guards
|
||||
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$guards)
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/TrimStrings.php
Normal file
19
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
20
app/Http/Middleware/TrustHosts.php
Normal file
20
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts()
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Middleware/TrustProxies.php
Normal file
28
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies = '*';
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
165
app/Http/Requests/Admin/GroupRequest.php
Normal file
165
app/Http/Requests/Admin/GroupRequest.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Http\Requests\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class GroupRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$requiredIfReview = function ($driver) {
|
||||
return Rule::requiredIf($this->input('configs.is_enable_scan') && $this->input('configs.scan_configs.driver') === $driver);
|
||||
};
|
||||
|
||||
$requiredIfWatermark = function ($driver) {
|
||||
return Rule::requiredIf($this->input('configs.is_enable_watermark') && $this->input('configs.watermark_configs.driver') === $driver);
|
||||
};
|
||||
|
||||
return [
|
||||
'name' => 'required|between:2,30',
|
||||
'is_default' => 'boolean',
|
||||
'is_guest' => 'boolean',
|
||||
'configs' => 'required|array',
|
||||
'configs.maximum_file_size' => 'required|numeric',
|
||||
'configs.concurrent_upload_num' => 'required|integer',
|
||||
'configs.limit_per_minute' => 'required|integer',
|
||||
'configs.limit_per_hour' => 'required|integer',
|
||||
'configs.limit_per_day' => 'required|integer',
|
||||
'configs.limit_per_week' => 'required|integer',
|
||||
'configs.limit_per_month' => 'required|integer',
|
||||
'configs.image_save_quality' => 'required|min:1|max:100',
|
||||
'configs.image_save_format' => '',
|
||||
'configs.path_naming_rule' => 'max:400',
|
||||
'configs.file_naming_rule' => 'max:400',
|
||||
'configs.accepted_file_suffixes' => 'required|array|in:jpeg,jpg,png,gif,tif,bmp,ico,psd,webp,svg',
|
||||
|
||||
'configs.is_enable_scan' => 'boolean',
|
||||
'configs.scanned_action' => [
|
||||
'exclude_if:configs.is_enable_scan,false',
|
||||
'in:mark,delete',
|
||||
],
|
||||
'configs.scan_configs.driver' => ['exclude_if:configs.is_enable_scan,false', 'in:tencent,aliyun,nsfwjs'],
|
||||
'configs.scan_configs.drivers.tencent.endpoint' => [$requiredIfReview('tencent')],
|
||||
'configs.scan_configs.drivers.tencent.secret_id' => [$requiredIfReview('tencent')],
|
||||
'configs.scan_configs.drivers.tencent.secret_key' => [$requiredIfReview('tencent')],
|
||||
'configs.scan_configs.drivers.tencent.region' => [$requiredIfReview('tencent')],
|
||||
'configs.scan_configs.drivers.tencent.biz_type' => '',
|
||||
|
||||
'configs.scan_configs.drivers.aliyun.access_key_id' => [$requiredIfReview('aliyun')],
|
||||
'configs.scan_configs.drivers.aliyun.access_key_secret' => [$requiredIfReview('aliyun')],
|
||||
'configs.scan_configs.drivers.aliyun.region_id' => [$requiredIfReview('aliyun')],
|
||||
'configs.scan_configs.drivers.aliyun.biz_type' => '',
|
||||
'configs.scan_configs.drivers.aliyun.scenes' => [$requiredIfReview('aliyun'), 'array'],
|
||||
|
||||
'configs.scan_configs.drivers.nsfwjs.api_url' => [$requiredIfReview('nsfwjs')],
|
||||
'configs.scan_configs.drivers.nsfwjs.attr_name' => [$requiredIfReview('nsfwjs'), 'nullable'],
|
||||
'configs.scan_configs.drivers.nsfwjs.threshold' => [$requiredIfReview('nsfwjs'), 'nullable', 'integer', 'between:1,100'],
|
||||
|
||||
'configs.is_enable_original_protection' => 'boolean',
|
||||
'configs.image_cache_ttl' => 'nullable|numeric',
|
||||
|
||||
'configs.is_enable_watermark' => 'boolean',
|
||||
'configs.watermark_configs.mode' => ['in:1,2'],
|
||||
'configs.watermark_configs.driver' => ['exclude_if:configs.is_enable_watermark,false', 'in:font,image'],
|
||||
'configs.watermark_configs.drivers.font.font' => [
|
||||
$requiredIfWatermark('font'),
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! file_exists(storage_path('app/public/'.$value))) {
|
||||
$fail('字体文件不存在');
|
||||
}
|
||||
},
|
||||
],
|
||||
'configs.watermark_configs.drivers.font.position' => [$requiredIfWatermark('font')],
|
||||
'configs.watermark_configs.drivers.font.text' => [$requiredIfWatermark('font')],
|
||||
'configs.watermark_configs.drivers.font.color' => [$requiredIfWatermark('font')],
|
||||
'configs.watermark_configs.drivers.font.size' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.font.angle' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.font.x' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.font.y' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.image.image' => [
|
||||
$requiredIfWatermark('image'),
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! file_exists(storage_path('app/public/'.$value))) {
|
||||
$fail('图片文件不存在');
|
||||
}
|
||||
},
|
||||
],
|
||||
'configs.watermark_configs.drivers.image.position' => [$requiredIfWatermark('image')],
|
||||
'configs.watermark_configs.drivers.image.width' => [$requiredIfWatermark('image'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.image.height' => [$requiredIfWatermark('image'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.image.opacity' => [
|
||||
$requiredIfWatermark('image'), 'nullable', 'integer', 'between:0,100',
|
||||
],
|
||||
'configs.watermark_configs.drivers.image.rotate' => [$requiredIfWatermark('image'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.image.x' => [$requiredIfWatermark('image'), 'nullable', 'integer'],
|
||||
'configs.watermark_configs.drivers.image.y' => [$requiredIfWatermark('image'), 'nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
return [
|
||||
'name' => '组名称',
|
||||
'is_default' => '是否默认',
|
||||
'configs' => '组配置',
|
||||
'configs.maximum_file_size' => '文件最大上传大小',
|
||||
'configs.concurrent_upload_num' => '并发上传数量',
|
||||
'configs.limit_per_minute' => '每分钟上传限制',
|
||||
'configs.limit_per_hour' => '每小时上传限制',
|
||||
'configs.limit_per_day' => '每天上传限制',
|
||||
'configs.limit_per_week' => '每周上传限制',
|
||||
'configs.limit_per_month' => '每月上传限制',
|
||||
'configs.path_naming_rule' => '路径命名规则',
|
||||
'configs.file_naming_rule' => '文件命名规则',
|
||||
'configs.image_save_quality' => '图片保存质量',
|
||||
'configs.image_save_format' => '图片保存格式',
|
||||
'configs.accepted_file_suffixes' => '允许上传的文件后缀',
|
||||
|
||||
'configs.is_enable_scan' => '是否启用图片审核',
|
||||
'configs.scanned_action' => '图片审核动作',
|
||||
'configs.scan_configs.driver' => '图片审核驱动',
|
||||
'configs.scan_configs.drivers.tencent.endpoint' => 'Endpoint',
|
||||
'configs.scan_configs.drivers.tencent.secret_id' => 'SecretId',
|
||||
'configs.scan_configs.drivers.tencent.secret_key' => 'SecretKey',
|
||||
'configs.scan_configs.drivers.tencent.region' => '地域节点',
|
||||
'configs.scan_configs.drivers.tencent.biz_type' => '业务场景',
|
||||
'configs.scan_configs.drivers.aliyun.access_key_id' => 'AccessKeyId',
|
||||
'configs.scan_configs.drivers.aliyun.access_key_secret' => 'AccessKeySecret',
|
||||
'configs.scan_configs.drivers.aliyun.region_id' => '地域节点',
|
||||
'configs.scan_configs.drivers.aliyun.biz_type' => '场景名称',
|
||||
'configs.scan_configs.drivers.aliyun.scenes' => '审核场景',
|
||||
'configs.scan_configs.drivers.nsfwjs.api_url' => '接口地址',
|
||||
'configs.scan_configs.drivers.nsfwjs.attr_name' => '表单名称',
|
||||
'configs.scan_configs.drivers.nsfwjs.threshold' => '阈值',
|
||||
|
||||
'configs.is_enable_original_protection' => '是否启用原图保护功能',
|
||||
'configs.image_cache_ttl' => '图片缓存时间',
|
||||
|
||||
'configs.is_enable_watermark' => '是否启用水印功能',
|
||||
'configs.watermark_configs.driver' => '水印驱动',
|
||||
'configs.watermark_configs.drivers.font.font' => '字体文件',
|
||||
'configs.watermark_configs.drivers.font.position' => '水印位置',
|
||||
'configs.watermark_configs.drivers.font.text' => '水印文字',
|
||||
'configs.watermark_configs.drivers.font.color' => '字体颜色',
|
||||
'configs.watermark_configs.drivers.font.size' => '水印文字大小',
|
||||
'configs.watermark_configs.drivers.font.angle' => '水印旋转角度',
|
||||
'configs.watermark_configs.drivers.font.x' => '水印X轴偏移量',
|
||||
'configs.watermark_configs.drivers.font.y' => '水印Y轴偏移量',
|
||||
'configs.watermark_configs.drivers.image.image' => '水印图片文件',
|
||||
'configs.watermark_configs.drivers.image.position' => '水印位置',
|
||||
'configs.watermark_configs.drivers.image.width' => '水印图片宽度',
|
||||
'configs.watermark_configs.drivers.image.height' => '水印图片高度',
|
||||
'configs.watermark_configs.drivers.image.opacity' => '水印透明度',
|
||||
'configs.watermark_configs.drivers.image.rotate' => '水印旋转角度',
|
||||
'configs.watermark_configs.drivers.image.x' => '水印X轴偏移量',
|
||||
'configs.watermark_configs.drivers.image.y' => '水印Y轴偏移量',
|
||||
];
|
||||
}
|
||||
}
|
||||
221
app/Http/Requests/Admin/StrategyRequest.php
Normal file
221
app/Http/Requests/Admin/StrategyRequest.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Enums\StrategyKey;
|
||||
use App\Http\Requests\FormRequest;
|
||||
use App\Models\Strategy;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class StrategyRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$checkUrl = function ($attribute, $value, $fail) {
|
||||
if ($this->input('key') == StrategyKey::Local) {
|
||||
$folders = [config('app.thumbnail_path'), 'fonts', 'css', 'js'];
|
||||
$symlink = Strategy::getRootPath($value);
|
||||
if (! $symlink) {
|
||||
return $fail('访问域名缺少根路径');
|
||||
}
|
||||
if (in_array($symlink, $folders)) {
|
||||
return $fail('系统保留路径:'. $symlink);
|
||||
}
|
||||
if (false !== strpbrk($symlink, "\\/?%*:|\"<>")) {
|
||||
return $fail('根路径名称不符合规则');
|
||||
}
|
||||
// 修改时单独判断
|
||||
if ($this->method() === 'PUT') {
|
||||
$symlinks = Strategy::query()
|
||||
->where('id', '<>', $this->route('id'))
|
||||
->pluck('configs')
|
||||
->transform(function (Collection $configs) {
|
||||
return Strategy::getRootPath($configs->get('url'));
|
||||
})->merge($folders)->values()->toArray();
|
||||
if (in_array($symlink, $symlinks)) {
|
||||
return $fail('根路径已经存在');
|
||||
}
|
||||
} else {
|
||||
if (realpath(public_path($symlink))) {
|
||||
return $fail('根路径已经存在');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$array = [
|
||||
'groups' => 'array',
|
||||
'name' => 'required|max:60',
|
||||
'intro' => 'max:2000',
|
||||
'key' => 'required|integer',
|
||||
'configs.url' => ['required', 'url'],
|
||||
'configs.queries' => '',
|
||||
];
|
||||
|
||||
return array_merge($array, match((int)$this->input('key')) {
|
||||
StrategyKey::Local => [
|
||||
'configs.url' => ['required', 'url', $checkUrl],
|
||||
'configs.root' => ['max:1000', function ($attribute, $value, $fail) {
|
||||
if ($value) {
|
||||
if (! is_dir($value)) {
|
||||
return $fail('储存路径不存在');
|
||||
}
|
||||
if (! is_writeable($value)) {
|
||||
return $fail('储存路径没有写入权限');
|
||||
}
|
||||
}
|
||||
}],
|
||||
],
|
||||
StrategyKey::S3 => [
|
||||
'configs.access_key_id' => 'required',
|
||||
'configs.secret_access_key' => 'required',
|
||||
'configs.endpoint' => '',
|
||||
'configs.region' => '',
|
||||
'configs.bucket' => 'required',
|
||||
],
|
||||
StrategyKey::Oss => [
|
||||
'configs.access_key_id' => 'required',
|
||||
'configs.access_key_secret' => 'required',
|
||||
'configs.endpoint' => 'required',
|
||||
'configs.bucket' => 'required',
|
||||
],
|
||||
StrategyKey::Cos => [
|
||||
'configs.app_id' => 'required',
|
||||
'configs.secret_id' => 'required',
|
||||
'configs.secret_key' => 'required',
|
||||
'configs.region' => 'required',
|
||||
'configs.bucket' => 'required',
|
||||
],
|
||||
StrategyKey::Kodo => [
|
||||
'configs.access_key' => 'required',
|
||||
'configs.secret_key' => 'required',
|
||||
'configs.bucket' => 'required',
|
||||
],
|
||||
StrategyKey::Uss => [
|
||||
'configs.service' => 'required',
|
||||
'configs.operator' => 'required',
|
||||
'configs.password' => 'required',
|
||||
],
|
||||
StrategyKey::Sftp => [
|
||||
'configs.root' => 'required',
|
||||
'configs.host' => 'required',
|
||||
'configs.port' => 'integer',
|
||||
'configs.username' => 'required',
|
||||
'configs.password' => 'required_if:configs.private_key,null',
|
||||
'configs.private_key' => 'required_if:configs.password,null',
|
||||
'configs.passphrase' => '',
|
||||
'configs.use_agent' => 'required|boolean',
|
||||
],
|
||||
StrategyKey::Ftp => [
|
||||
'configs.root' => 'required',
|
||||
'configs.host' => 'required',
|
||||
'configs.port' => 'integer',
|
||||
'configs.username' => 'required',
|
||||
'configs.password' => 'required',
|
||||
'configs.ssl' => 'required|boolean',
|
||||
'configs.passive' => 'required|boolean',
|
||||
],
|
||||
StrategyKey::Webdav => [
|
||||
'configs.base_uri' => 'required',
|
||||
'configs.username' => '',
|
||||
'configs.password' => '',
|
||||
'configs.auth_type' => '',
|
||||
'configs.prefix' => '',
|
||||
],
|
||||
StrategyKey::Minio => [
|
||||
'configs.access_key' => 'required',
|
||||
'configs.secret_key' => 'required',
|
||||
'configs.endpoint' => '',
|
||||
'configs.region' => '',
|
||||
'configs.bucket' => 'required',
|
||||
'configs.bucket_endpoint' => '',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
$array = [
|
||||
'name' => '名称',
|
||||
'intro' => '简介',
|
||||
'key' => '策略',
|
||||
'configs.url' => '访问网址',
|
||||
'configs.queries' => 'Url 额外参数',
|
||||
];
|
||||
|
||||
return array_merge($array, match((int)$this->input('key')) {
|
||||
StrategyKey::Local => [
|
||||
'configs.root' => '根目录路径',
|
||||
],
|
||||
StrategyKey::S3 => [
|
||||
'configs.access_key_id' => 'AccessKeyId',
|
||||
'configs.secret_access_key' => 'SecretAccessKey',
|
||||
'configs.endpoint' => '连接地址',
|
||||
'configs.region' => '区域',
|
||||
'configs.bucket' => '储存桶名称',
|
||||
],
|
||||
StrategyKey::Oss => [
|
||||
'configs.access_key_id' => 'AccessKeyId',
|
||||
'configs.access_key_secret' => 'AccessKeySecret',
|
||||
'configs.endpoint' => '地域节点域名',
|
||||
'configs.bucket' => 'Bucket 名称',
|
||||
],
|
||||
StrategyKey::Cos => [
|
||||
'configs.app_id' => 'AppId',
|
||||
'configs.secret_id' => 'SecretId',
|
||||
'configs.secret_key' => 'SecretKey',
|
||||
'configs.region' => '所属地域',
|
||||
'configs.bucket' => '储存桶名称',
|
||||
],
|
||||
StrategyKey::Kodo => [
|
||||
'configs.access_key' => 'AccessKey',
|
||||
'configs.secret_key' => 'SecretKey',
|
||||
'configs.bucket' => 'Bucket',
|
||||
],
|
||||
StrategyKey::Uss => [
|
||||
'configs.service' => '服务名称',
|
||||
'configs.operator' => '操作员名称',
|
||||
'configs.password' => '操作员密码',
|
||||
],
|
||||
StrategyKey::Sftp => [
|
||||
'configs.root' => '根目录路径',
|
||||
'configs.host' => '主机地址',
|
||||
'configs.port' => '连接端口',
|
||||
'configs.username' => '用户名',
|
||||
'configs.password' => '密码',
|
||||
'configs.private_key' => '密钥',
|
||||
'configs.passphrase' => '密钥口令',
|
||||
'configs.use_agent' => '使用代理',
|
||||
],
|
||||
StrategyKey::Ftp => [
|
||||
'configs.root' => '根目录路径',
|
||||
'configs.host' => '主机地址',
|
||||
'configs.port' => '连接端口',
|
||||
'configs.username' => '用户名',
|
||||
'configs.password' => '密码',
|
||||
'configs.ssl' => '加密连接',
|
||||
'configs.passive' => '被动模式',
|
||||
],
|
||||
StrategyKey::Webdav => [
|
||||
'configs.base_uri' => '连接地址',
|
||||
'configs.username' => '用户名',
|
||||
'configs.password' => '密码',
|
||||
'configs.auth_type' => '认证方式',
|
||||
'configs.prefix' => '前缀',
|
||||
],
|
||||
StrategyKey::Minio => [
|
||||
'configs.access_key' => 'AccessKey',
|
||||
'configs.secret_key' => 'SecretKey',
|
||||
'configs.endpoint' => '连接地址',
|
||||
'configs.region' => '区域',
|
||||
'configs.bucket' => 'Bucket 名称',
|
||||
'configs.bucket_endpoint' => 'BucketEndpoint',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Admin/UserRequest.php
Normal file
36
app/Http/Requests/Admin/UserRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Http\Requests\FormRequest;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class UserRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'group_id' => 'required',
|
||||
'name' => 'required|between:2,30',
|
||||
'capacity' => 'required|numeric',
|
||||
'password' => ['nullable', Rules\Password::defaults()],
|
||||
'status' => 'in:1,0'
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
return [
|
||||
'group_id' => '角色组',
|
||||
'name' => '昵称',
|
||||
'capacity' => '总容量',
|
||||
'password' => '密码',
|
||||
'status' => '状态',
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/AlbumRequest.php
Normal file
29
app/Http/Requests/AlbumRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class AlbumRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|max:60|alpha_dash',
|
||||
'intro' => 'max:600'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => '名称不能为空',
|
||||
'name.max' => '名称字符过长,最大不能超过 60',
|
||||
'name.alpha_dash' => '名称只能是字母、数字,短破折号(-)和下划线(_)',
|
||||
'intro.max' => '简介字符过长,最大不能超过 600'
|
||||
];
|
||||
}
|
||||
}
|
||||
93
app/Http/Requests/Auth/LoginRequest.php
Normal file
93
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited()
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function throttleKey()
|
||||
{
|
||||
return Str::lower($this->input('email')).'|'.$this->ip();
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/FormRequest.php
Normal file
22
app/Http/Requests/FormRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Http\Result;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
|
||||
class FormRequest extends \Illuminate\Foundation\Http\FormRequest
|
||||
{
|
||||
use Result;
|
||||
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw (new HttpResponseException($this->fail($validator->errors()->first())));
|
||||
}
|
||||
}
|
||||
40
app/Http/Requests/ImageRenameRequest.php
Normal file
40
app/Http/Requests/ImageRenameRequest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class ImageRenameRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'id' => 'required|numeric',
|
||||
'name' => 'required|max:50|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'id.required' => '请选择一张图片',
|
||||
'id.numeric' => '图片选择异常',
|
||||
'name.required' => '请输入名称',
|
||||
'name.max' => '名称长度不能超过 50 个字符',
|
||||
'name.string' => '名称格式不正确',
|
||||
];
|
||||
}
|
||||
}
|
||||
46
app/Http/Requests/UserSettingRequest.php
Normal file
46
app/Http/Requests/UserSettingRequest.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class UserSettingRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'name' => 'required|between:2,20',
|
||||
'url' => 'nullable|url',
|
||||
'password' => 'nullable|between:6,32',
|
||||
'configs' => 'array',
|
||||
'configs.default_album' => 'required|numeric',
|
||||
'configs.default_strategy' => 'required|numeric',
|
||||
'configs.default_permission' => 'required|in:1,0',
|
||||
'configs.pasted_action' => 'required|in:1,2',
|
||||
'configs.is_auto_clear_preview' => 'nullable|boolean'
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'name.required' => '昵称不能为空',
|
||||
'name.between' => '昵称必须在 2-20 个字符之间',
|
||||
'url.url' => '个人主页地址格式不正确',
|
||||
'password.between' => '密码必须在 6-32 个字符之间',
|
||||
'configs.array' => '配置值不正确',
|
||||
'configs.default_album.required' => '默认相册选择错误',
|
||||
'configs.default_album.numeric' => '默认相册选择错误',
|
||||
'configs.default_strategy.required' => '默认策略选择错误',
|
||||
'configs.default_strategy.numeric' => '默认策略选择错误',
|
||||
'configs.default_permission.required' => '权限值选择错误',
|
||||
'configs.default_permission.in' => '权限值不正确',
|
||||
'configs.pasted_action.required' => '粘贴动作值选择错误',
|
||||
'configs.pasted_action.in' => '粘贴动作值不正确',
|
||||
'configs.is_auto_clear_preview.boolean' => '是否自动清除预览选择错误'
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Result.php
Normal file
24
app/Http/Result.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
trait Result
|
||||
{
|
||||
public function success(string $message = 'success', $data = []): Response
|
||||
{
|
||||
return $this->response(true, $message, $data);
|
||||
}
|
||||
|
||||
public function fail(string $message = 'fail', $data = []): Response
|
||||
{
|
||||
return $this->response(false, $message, $data);
|
||||
}
|
||||
|
||||
public function response(bool $status, string $message = '', $data = []): Response
|
||||
{
|
||||
$data = $data ?: new \stdClass;
|
||||
return response(compact('status', 'message', 'data'));
|
||||
}
|
||||
}
|
||||
33
app/Mail/Test.php
Normal file
33
app/Mail/Test.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Test extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->view('emails.test');
|
||||
}
|
||||
}
|
||||
75
app/Models/Album.php
Normal file
75
app/Models/Album.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $name
|
||||
* @property string $intro
|
||||
* @property int $image_num
|
||||
* @property-read User $user
|
||||
* @property-read Collection $images
|
||||
*/
|
||||
class Album extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'intro',
|
||||
'image_num'
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'intro' => '',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'image_num' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeFilter(Builder $builder, Request $request)
|
||||
{
|
||||
return $builder->when($request->query('order') ?: 'newest', function (Builder $builder, $order) {
|
||||
switch ($order) {
|
||||
case 'earliest':
|
||||
$builder->orderBy('created_at');
|
||||
break;
|
||||
case 'most':
|
||||
$builder->orderByDesc('image_num');
|
||||
break;
|
||||
case 'least':
|
||||
$builder->orderBy('image_num');
|
||||
break;
|
||||
default:
|
||||
$builder->latest();
|
||||
}
|
||||
})->when($request->query('keyword'), function (Builder $builder, $keyword) {
|
||||
$builder->where('name', 'like', "%{$keyword}%")->orWhere('intro', 'like', "%{$keyword}%");
|
||||
});
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(Image::class, 'album_id', 'id');
|
||||
}
|
||||
}
|
||||
18
app/Models/Config.php
Normal file
18
app/Models/Config.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
/**
|
||||
* @property string $name
|
||||
* @property mixed $value
|
||||
*/
|
||||
class Config extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
}
|
||||
101
app/Models/Group.php
Normal file
101
app/Models/Group.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property boolean $is_default
|
||||
* @property boolean $is_guest
|
||||
* @property Collection $configs
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $users
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $images
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $strategies
|
||||
*/
|
||||
class Group extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'is_default',
|
||||
'is_guest',
|
||||
'configs',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'is_default' => 'bool',
|
||||
'is_guest' => 'bool',
|
||||
'configs' => 'collection'
|
||||
];
|
||||
|
||||
const POSITIONS = [
|
||||
'top-left' => '左上角',
|
||||
'top' => '上中',
|
||||
'top-right' => '右上角',
|
||||
'left' => '左边',
|
||||
'center' => '中间',
|
||||
'right' => '右边',
|
||||
'bottom-left' => '左下角',
|
||||
'bottom' => '下中',
|
||||
'bottom-right' => '右下角',
|
||||
'tiled' => '平铺',
|
||||
];
|
||||
|
||||
const SCENES = [
|
||||
'porn' => '智能鉴黄',
|
||||
'terrorism' => '暴恐涉政',
|
||||
'ad' => '图文违规',
|
||||
'qrcode' => '二维码',
|
||||
'live' => '不良场景',
|
||||
'logo' => 'Logo',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function (self $group) {
|
||||
if ($group->isDirty('is_default') && $group->is_default) {
|
||||
Group::query()->where('is_default', true)->update(['is_default' => false]);
|
||||
}
|
||||
if ($group->isDirty('is_guest') && $group->is_guest) {
|
||||
Group::query()->where('is_guest', true)->update(['is_guest' => false]);
|
||||
}
|
||||
$group->configs = Utils::parseConfigs(self::getDefaultConfigs()->toArray(), $group->configs->toArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组默认配置
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function getDefaultConfigs(): Collection
|
||||
{
|
||||
return collect(config('convention.group'));
|
||||
}
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(Image::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function strategies(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Strategy::class, 'group_strategy', 'group_id', 'strategy_id');
|
||||
}
|
||||
}
|
||||
254
app/Models/Image.php
Normal file
254
app/Models/Image.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\GroupConfigKey;
|
||||
use App\Enums\ImagePermission;
|
||||
use App\Services\ImageService;
|
||||
use App\Utils;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Flysystem\Filesystem;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property int $album_id
|
||||
* @property int $group_id
|
||||
* @property int $strategy_id
|
||||
* @property string $key
|
||||
* @property string $path
|
||||
* @property string $name
|
||||
* @property string $pathname
|
||||
* @property string $origin_name
|
||||
* @property string $alias_name
|
||||
* @property string $filename
|
||||
* @property float $size
|
||||
* @property string $mimetype
|
||||
* @property string $extension
|
||||
* @property string $md5
|
||||
* @property string $sha1
|
||||
* @property integer $width
|
||||
* @property integer $height
|
||||
* @property string $url
|
||||
* @property string $thumb_url
|
||||
* @property Collection $links
|
||||
* @property int $permission
|
||||
* @property boolean $is_unhealthy
|
||||
* @property string $uploaded_ip
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property-read User $user
|
||||
* @property-read Album $album
|
||||
* @property-read Group $group
|
||||
* @property-read Strategy $strategy
|
||||
*/
|
||||
class Image extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'path',
|
||||
'name',
|
||||
'origin_name',
|
||||
'alias_name',
|
||||
'size',
|
||||
'mimetype',
|
||||
'extension',
|
||||
'md5',
|
||||
'sha1',
|
||||
'width',
|
||||
'height',
|
||||
'permission',
|
||||
'is_unhealthy',
|
||||
'uploaded_ip',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'user_id',
|
||||
'album_id',
|
||||
'group_id',
|
||||
'strategy_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'album_id' => 'integer',
|
||||
'group_id' => 'integer',
|
||||
'strategy_id' => 'integer',
|
||||
'width' => 'integer',
|
||||
'height' => 'integer',
|
||||
'size' => 'float',
|
||||
'is_unhealthy' => 'bool',
|
||||
'permission' => 'integer',
|
||||
];
|
||||
|
||||
protected ?Filesystem $filesystem = null;
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (self $image) {
|
||||
$image->key = $image->generateKey();
|
||||
});
|
||||
|
||||
static::deleting(function (self $image) {
|
||||
// TODO 检测是否启用了队列,放置队列中异步删除
|
||||
// 在当前图片所属的策略中是否存在其他相同 md5 和 sha1 的记录,没有则可以删除物理文件
|
||||
if (!static::query()
|
||||
->where('strategy_id', $image->strategy_id)
|
||||
->where('id', '<>', $image->id)
|
||||
->where('md5', $image->md5)
|
||||
->where('sha1', $image->sha1)
|
||||
->exists()
|
||||
) {
|
||||
// 删除本地缓存文件
|
||||
try {
|
||||
// 删除物理文件
|
||||
$image->filesystem()->delete($image->pathname);
|
||||
@unlink(public_path($image->getThumbnailPathname()));
|
||||
// 删除缓存
|
||||
Cache::forget("image_{$image->key}");
|
||||
} catch (\Throwable $e) {
|
||||
Utils::e($e, '删除物理文件时发生异常');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeFilter(Builder $builder, Request $request)
|
||||
{
|
||||
return $builder->when($request->query('order') ?: 'newest', function (Builder $builder, $order) {
|
||||
switch ($order) {
|
||||
case 'earliest':
|
||||
$builder->orderBy('created_at');
|
||||
break;
|
||||
case 'utmost':
|
||||
$builder->orderByDesc('size');
|
||||
break;
|
||||
case 'least':
|
||||
$builder->orderBy('size');
|
||||
break;
|
||||
default:
|
||||
$builder->latest();
|
||||
}
|
||||
})->when($request->query('permission') ?: 'all', function (Builder $builder, $permission) {
|
||||
switch ($permission) {
|
||||
case 'public':
|
||||
$builder->where('permission', ImagePermission::Public);
|
||||
break;
|
||||
case 'private':
|
||||
$builder->where('permission', ImagePermission::Private);
|
||||
break;
|
||||
}
|
||||
})->when($request->query('keyword'), function (Builder $builder, $keyword) {
|
||||
$builder->where('origin_name', 'like', "%{$keyword}%")->orWhere('alias_name', 'like', "%{$keyword}%");
|
||||
})->when((int) $request->query('album_id'), function (Builder $builder, $albumId) {
|
||||
$builder->where('album_id', $albumId);
|
||||
}, function (Builder $builder) {
|
||||
$builder->whereNull('album_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function filename(): Attribute
|
||||
{
|
||||
return new Attribute(fn() => $this->alias_name ?: $this->origin_name);
|
||||
}
|
||||
|
||||
public function pathname(): Attribute
|
||||
{
|
||||
$path = $this->path ? "{$this->path}/" : '';
|
||||
return new Attribute(fn() => "{$path}{$this->name}");
|
||||
}
|
||||
|
||||
public function url(): Attribute
|
||||
{
|
||||
return new Attribute(function () {
|
||||
// 是否启用原图保护功能
|
||||
if ($this->group?->configs->get(GroupConfigKey::IsEnableOriginalProtection)) {
|
||||
$url = asset("{$this->key}.{$this->extension}");
|
||||
} else {
|
||||
$url = rtrim($this->strategy?->configs->get('url'), '/').'/'.ltrim($this->pathname, '/');
|
||||
}
|
||||
|
||||
// 拼接图片 url
|
||||
return $url.($this->strategy?->configs->get('queries') ?: '');
|
||||
});
|
||||
}
|
||||
|
||||
public function thumbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(function () {
|
||||
$pathname = $this->getThumbnailPathname();
|
||||
|
||||
// 没有缩略图则返回原图
|
||||
if (! file_exists(public_path($pathname))) {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
return asset($pathname);
|
||||
});
|
||||
}
|
||||
|
||||
public function links(): Attribute
|
||||
{
|
||||
return new Attribute(fn() => collect([
|
||||
'url' => $this->url,
|
||||
'html' => "<img src=\"{$this->url}\" alt=\"{$this->origin_name}\" title=\"{$this->origin_name}\" />",
|
||||
'bbcode' => "[img]{$this->url}[/img]",
|
||||
'markdown' => "",
|
||||
'markdown_with_link' => "[]({$this->url})",
|
||||
'thumbnail_url' => $this->thumb_url,
|
||||
]));
|
||||
}
|
||||
|
||||
public function filesystem(): Filesystem
|
||||
{
|
||||
if (is_null($this->filesystem)) {
|
||||
$this->filesystem = new Filesystem((new ImageService())->getAdapter($this->strategy));
|
||||
}
|
||||
return $this->filesystem;
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function album(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Album::class, 'album_id', 'id');
|
||||
}
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Group::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function strategy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Strategy::class, 'strategy_id', 'id');
|
||||
}
|
||||
|
||||
public function getThumbnailPathname(): string
|
||||
{
|
||||
return trim(config('app.thumbnail_path'), '/')."/{$this->md5}.". ($this->extension === 'svg' ? 'svg' : "png");
|
||||
}
|
||||
|
||||
private function generateKey($length = 6): string
|
||||
{
|
||||
$key = Str::random($length);
|
||||
if (self::query()->where('key', $key)->exists()) {
|
||||
return $this->generateKey(++$length);
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
13
app/Models/Model.php
Normal file
13
app/Models/Model.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
|
||||
abstract class Model extends \Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
protected function serializeDate(\DateTimeInterface $date): string
|
||||
{
|
||||
return $date->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT);
|
||||
}
|
||||
}
|
||||
120
app/Models/Strategy.php
Normal file
120
app/Models/Strategy.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StrategyKey;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Sabre\DAV\Client;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $key
|
||||
* @property string $name
|
||||
* @property string $intro
|
||||
* @property \Illuminate\Support\Collection $configs
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property-read Collection $groups
|
||||
* @property-read Collection $images
|
||||
*/
|
||||
class Strategy extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'name',
|
||||
'intro',
|
||||
'configs',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'intro' => '',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'key' => 'integer',
|
||||
'configs' => 'collection',
|
||||
];
|
||||
|
||||
const DRIVERS = [
|
||||
StrategyKey::Local => '本地',
|
||||
StrategyKey::S3 => 'AWS S3',
|
||||
StrategyKey::Oss => '阿里云 OSS',
|
||||
StrategyKey::Cos => '腾讯云 COS',
|
||||
StrategyKey::Kodo => '七牛云 Kodo',
|
||||
StrategyKey::Uss => '又拍云 USS',
|
||||
StrategyKey::Sftp => 'SFTP',
|
||||
StrategyKey::Ftp => 'FTP',
|
||||
StrategyKey::Webdav => 'WebDav',
|
||||
StrategyKey::Minio => 'Minio',
|
||||
];
|
||||
|
||||
const WEBDAV_AUTH_TYPES = [
|
||||
'' => 'Auto',
|
||||
Client::AUTH_BASIC => 'Basic',
|
||||
Client::AUTH_DIGEST => 'Digest',
|
||||
Client::AUTH_NTLM => 'Ntlm',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function (self $strategy) {
|
||||
$strategy->configs['root'] = $strategy->configs->get('root');
|
||||
$strategy->configs['url'] = rtrim($strategy->configs->get('url'), '/');
|
||||
if ($strategy->key == StrategyKey::Local) {
|
||||
$symlink = self::getRootPath($strategy->configs['url']);
|
||||
$target = $strategy->configs['root'] ?: config('filesystems.disks.uploads.root');
|
||||
if (! is_dir(public_path($symlink))) {
|
||||
(new Filesystem())->link($target, $symlink);
|
||||
}
|
||||
// 是否需要移除旧的符号链接
|
||||
$url = $strategy->getOriginal('configs')['url'] ?? '';
|
||||
if ($url) {
|
||||
$oldSymlink = self::getRootPath($url);
|
||||
if ($oldSymlink != $symlink) {
|
||||
@unlink($oldSymlink);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function (self $strategy) {
|
||||
// 如果是本地策略,删除的时候同时删除符号连接
|
||||
if ($strategy->key === StrategyKey::Local) {
|
||||
$symlink = self::getRootPath($strategy->configs['url']);
|
||||
@unlink(public_path($symlink));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function getRootPath($url): string
|
||||
{
|
||||
$path = (parse_url($url)['path'] ?? '');
|
||||
return current(array_filter(explode('/', $path)));
|
||||
}
|
||||
|
||||
public function intro(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
set: fn ($value) => $value ?: '',
|
||||
);
|
||||
}
|
||||
|
||||
public function groups(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Group::class, 'group_strategy', 'strategy_id', 'group_id');
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(Image::class, 'strategy_id', 'id');
|
||||
}
|
||||
}
|
||||
130
app/Models/User.php
Normal file
130
app/Models/User.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Utils;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $group_id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property string $remember_token
|
||||
* @property boolean $is_adminer
|
||||
* @property float $capacity
|
||||
* @property float $use_capacity
|
||||
* @property string $url
|
||||
* @property Collection $configs
|
||||
* @property int $image_num
|
||||
* @property int $album_num
|
||||
* @property string $registered_ip
|
||||
* @property int $status
|
||||
* @property Carbon $email_verified_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property-read string $avatar
|
||||
* @property-read Group $group
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $albums
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $images
|
||||
*/
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'url',
|
||||
'capacity',
|
||||
'configs',
|
||||
'configs->default_strategy',
|
||||
'registered_ip',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'configs',
|
||||
'group_id',
|
||||
'is_adminer',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'group_id' => 'integer',
|
||||
'image_num' => 'integer',
|
||||
'album_num' => 'integer',
|
||||
'status' => 'integer',
|
||||
'capacity' => 'float',
|
||||
'is_adminer' => 'bool',
|
||||
'configs' => 'collection',
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $appends = ['avatar'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (self $user) {
|
||||
// 默认组
|
||||
$user->group_id = Group::query()->where('is_default', true)->value('id');
|
||||
// 初始容量
|
||||
$user->capacity = Utils::config(ConfigKey::UserInitialCapacity);
|
||||
$user->configs = collect(config('convention.user'))->merge($user->configs ?: []);
|
||||
});
|
||||
}
|
||||
|
||||
public function avatar(): Attribute
|
||||
{
|
||||
return new Attribute(fn () => Utils::getAvatar($this->email));
|
||||
}
|
||||
|
||||
public function useCapacity(): Attribute
|
||||
{
|
||||
return new Attribute(fn () => $this->images()->sum('size'));
|
||||
}
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Group::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function albums(): HasMany
|
||||
{
|
||||
return $this->hasMany(Album::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function images(): HasMany
|
||||
{
|
||||
return $this->hasMany(Image::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
56
app/Providers/AppServiceProvider.php
Normal file
56
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Enums\ConfigKey;
|
||||
use App\Models\Group;
|
||||
use App\Utils;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// 是否需要生成 env 文件
|
||||
if (! file_exists(base_path('.env'))) {
|
||||
file_put_contents(base_path('.env'), file_get_contents(base_path('.env.example')));
|
||||
// 生成 key
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
// 如果已经安装程序,初始化一些配置
|
||||
if (file_exists(base_path('installed.lock'))) {
|
||||
// 覆盖默认配置
|
||||
Config::set('app.name', Utils::config(ConfigKey::AppName));
|
||||
Config::set('mail', array_merge(\config('mail'), Utils::config(ConfigKey::Mail)->toArray()));
|
||||
|
||||
View::composer('*', function (\Illuminate\View\View $view) {
|
||||
/** @var Group $group */
|
||||
$group = Auth::check() ? Auth::user()->group : Group::query()->where('is_guest', true)->first();
|
||||
$view->with([
|
||||
'_group' => $group,
|
||||
'_is_notice' => strip_tags(Utils::config(ConfigKey::SiteNotice)),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/Providers/AuthServiceProvider.php
Normal file
30
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
//
|
||||
}
|
||||
}
|
||||
21
app/Providers/BroadcastServiceProvider.php
Normal file
21
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
Broadcast::routes();
|
||||
|
||||
require base_path('routes/channels.php');
|
||||
}
|
||||
}
|
||||
32
app/Providers/EventServiceProvider.php
Normal file
32
app/Providers/EventServiceProvider.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event listener mappings for the application.
|
||||
*
|
||||
* @var array<class-string, array<int, class-string>>
|
||||
*/
|
||||
protected $listen = [
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user