mirror of
				https://github.com/9001/copyparty.git
				synced 2025-10-31 12:03:32 +00:00 
			
		
		
		
	Compare commits
	
		
			1610 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | cae5ccea62 | ||
|  | 3768cb4723 | ||
|  | 0815dce4c1 | ||
|  | a62f744a18 | ||
|  | 163e3fce46 | ||
|  | e76a50cb9d | ||
|  | 72fc76ef48 | ||
|  | c47047c30d | ||
|  | 3b8f66c0d5 | ||
|  | aa96a1acdc | ||
|  | 91cafc2511 | ||
|  | 23ca00bba8 | ||
|  | a75a992951 | ||
|  | 4fbd6853f4 | ||
|  | 71c3ad63b3 | ||
|  | e1324e37a5 | ||
|  | a996a09bba | ||
|  | 18c763ac08 | ||
|  | 3d9fb753ba | ||
|  | 714fd1811a | ||
|  | 4364581705 | ||
|  | ba02c9cc12 | ||
|  | 11eefaf968 | ||
|  | 5a968f9e47 | ||
|  | 6420c4bd03 | ||
|  | 0f9877201b | ||
|  | 9ba2dec9b2 | ||
|  | ae9cfea939 | ||
|  | cadaeeeace | ||
|  | 767696185b | ||
|  | c1efd227b7 | ||
|  | a50d0563c3 | ||
|  | e5641ddd16 | ||
|  | 700111ffeb | ||
|  | b8adeb824a | ||
|  | 30cc9defcb | ||
|  | 61875bd773 | ||
|  | 30905c6f5d | ||
|  | 9986136dfb | ||
|  | 1c0d978979 | ||
|  | 0a0364e9f8 | ||
|  | 3376fbde1a | ||
|  | ac21fa7782 | ||
|  | c1c8dc5e82 | ||
|  | 5a38311481 | ||
|  | 9f8edb7f32 | ||
|  | c5a6ac8417 | ||
|  | 50e01d6904 | ||
|  | 9b46291a20 | ||
|  | 14497b2425 | ||
|  | f7ceae5a5f | ||
|  | c9492d16ba | ||
|  | 9fb9ada3aa | ||
|  | db0abbfdda | ||
|  | e7f0009e57 | ||
|  | 4444f0f6ff | ||
|  | 418842d2d3 | ||
|  | cafe53c055 | ||
|  | 7673beef72 | ||
|  | b28bfe64c0 | ||
|  | 135ece3fbd | ||
|  | bd3640d256 | ||
|  | fc0405c8f3 | ||
|  | 7df890d964 | ||
|  | 8341041857 | ||
|  | 1b7634932d | ||
|  | 48a3898aa6 | ||
|  | 5d13ebb4ac | ||
|  | 015b87ee99 | ||
|  | 0a48acf6be | ||
|  | 2b6a3afd38 | ||
|  | 18aa82fb2f | ||
|  | f5407b2997 | ||
|  | 474d5a155b | ||
|  | afcd98b794 | ||
|  | 4f80e44ff7 | ||
|  | 406e413594 | ||
|  | 033b50ae1b | ||
|  | bee26e853b | ||
|  | 04a1f7040e | ||
|  | f9d5bb3b29 | ||
|  | ca0cd04085 | ||
|  | 999ee2e7bc | ||
|  | 1ff7f968e8 | ||
|  | 3966266207 | ||
|  | d03e96a392 | ||
|  | 4c843c6df9 | ||
|  | 0896c5295c | ||
|  | cc0c9839eb | ||
|  | d0aa20e17c | ||
|  | 1a658dedb7 | ||
|  | 8d376b854c | ||
|  | 490c16b01d | ||
|  | 2437a4e864 | ||
|  | 007d948cb9 | ||
|  | 335fcc8535 | ||
|  | 9eaa9904e0 | ||
|  | 0778da6c4d | ||
|  | a1bb10012d | ||
|  | 1441ccee4f | ||
|  | 491803d8b7 | ||
|  | 3dcc386b6f | ||
|  | 5aa54d1217 | ||
|  | 88b876027c | ||
|  | fcc3aa98fd | ||
|  | f2f5e266b4 | ||
|  | e17bf8f325 | ||
|  | d19cb32bf3 | ||
|  | 85a637af09 | ||
|  | 043e3c7dd6 | ||
|  | 8f59afb159 | ||
|  | 77f1e51444 | ||
|  | 22fc4bb938 | ||
|  | 50c7bba6ea | ||
|  | 551d99b71b | ||
|  | b54b7213a7 | ||
|  | a14943c8de | ||
|  | a10cad54fc | ||
|  | 8568b7702a | ||
|  | 5d8cb34885 | ||
|  | 8d248333e8 | ||
|  | 99e2ef7f33 | ||
|  | e767230383 | ||
|  | 90601314d6 | ||
|  | 9c5eac1274 | ||
|  | 50905439e4 | ||
|  | a0c1239246 | ||
|  | b8e851c332 | ||
|  | baaf2eb24d | ||
|  | e197895c10 | ||
|  | cb75efa05d | ||
|  | 8b0cf2c982 | ||
|  | fc7d9e1f9c | ||
|  | 10caafa34c | ||
|  | 22cc22225a | ||
|  | 22dff4b0e5 | ||
|  | a00ff2b086 | ||
|  | e4acddc23b | ||
|  | 2b2d8e4e02 | ||
|  | 5501d49032 | ||
|  | fa54b2eec4 | ||
|  | cb0160021f | ||
|  | 93a723d588 | ||
|  | 8ebe1fb5e8 | ||
|  | 2acdf685b1 | ||
|  | 9f122ccd16 | ||
|  | 03be26fafc | ||
|  | df5d309d6e | ||
|  | c355f9bd91 | ||
|  | 9c28ba417e | ||
|  | 705b58c741 | ||
|  | 510302d667 | ||
|  | 025a537413 | ||
|  | 60a1ff0fc0 | ||
|  | f94a0b1bff | ||
|  | 4ccfeeb2cd | ||
|  | 2646f6a4f2 | ||
|  | b286ab539e | ||
|  | 2cca6e0922 | ||
|  | db51f1b063 | ||
|  | d979c47f50 | ||
|  | e64b87b99b | ||
|  | b985011a00 | ||
|  | c2ed2314c8 | ||
|  | cd496658c3 | ||
|  | deca082623 | ||
|  | 0ea8bb7c83 | ||
|  | 1fb251a4c2 | ||
|  | 4295923b76 | ||
|  | 572aa4b26c | ||
|  | b1359f039f | ||
|  | 867d8ee49e | ||
|  | 04c86e8a89 | ||
|  | bc0cb43ef9 | ||
|  | 769454fdce | ||
|  | 4ee81af8f6 | ||
|  | 8b0e66122f | ||
|  | 8a98efb929 | ||
|  | b6fd555038 | ||
|  | 7eb413ad51 | ||
|  | 4421d509eb | ||
|  | 793ffd7b01 | ||
|  | 1e22222c60 | ||
|  | 544e0549bc | ||
|  | 83178d0836 | ||
|  | c44f5f5701 | ||
|  | 138f5bc989 | ||
|  | e4759f86ef | ||
|  | d71416437a | ||
|  | a84c583b2c | ||
|  | cdacdccdb8 | ||
|  | d3ccd3f174 | ||
|  | cb6de0387d | ||
|  | abff40519d | ||
|  | 55c74ad164 | ||
|  | 673b4f7e23 | ||
|  | d11e02da49 | ||
|  | 8790f89e08 | ||
|  | 33442026b8 | ||
|  | 03193de6d0 | ||
|  | 8675ff40f3 | ||
|  | d88889d3fc | ||
|  | 6f244d4335 | ||
|  | cacca663b3 | ||
|  | d5109be559 | ||
|  | d999f06bb9 | ||
|  | a1a8a8c7b5 | ||
|  | fdd6f3b4a6 | ||
|  | f5191973df | ||
|  | ddbaebe779 | ||
|  | 42099baeff | ||
|  | 2459965ca8 | ||
|  | 6acf436573 | ||
|  | f217e1ce71 | ||
|  | 418000aee3 | ||
|  | dbbba9625b | ||
|  | 397bc92fbc | ||
|  | 6e615dcd03 | ||
|  | 9ac5908b33 | ||
|  | 50912480b9 | ||
|  | 24b9b8319d | ||
|  | b0f4f0b653 | ||
|  | 05bbd41c4b | ||
|  | 8f5f8a3cda | ||
|  | c8938fc033 | ||
|  | 1550350e05 | ||
|  | 5cc190c026 | ||
|  | d6a0a738ce | ||
|  | f5fe3678ee | ||
|  | f2a7925387 | ||
|  | fa953ced52 | ||
|  | f0000d9861 | ||
|  | 4e67516719 | ||
|  | 29db7a6270 | ||
|  | 852499e296 | ||
|  | f1775fd51c | ||
|  | 4bb306932a | ||
|  | 2a37e81bd8 | ||
|  | 6a312ca856 | ||
|  | e7f3e475a2 | ||
|  | 854ba0ec06 | ||
|  | 209b49d771 | ||
|  | 949baae539 | ||
|  | 5f4ea27586 | ||
|  | 099cc97247 | ||
|  | 592b7d6315 | ||
|  | 0880bf55a1 | ||
|  | 4cbffec0ec | ||
|  | cc355417d4 | ||
|  | e2bc573e61 | ||
|  | 41c0376177 | ||
|  | c01cad091e | ||
|  | eb349f339c | ||
|  | 24d8caaf3e | ||
|  | 5ac2c20959 | ||
|  | bb72e6bf30 | ||
|  | d8142e866a | ||
|  | 7b7979fd61 | ||
|  | 749616d09d | ||
|  | 5485c6d7ca | ||
|  | b7aea38d77 | ||
|  | 0ecd9f99e6 | ||
|  | ca04a00662 | ||
|  | 8a09601be8 | ||
|  | 1fe0d4693e | ||
|  | bba8a3c6bc | ||
|  | e3d7f0c7d5 | ||
|  | be7bb71bbc | ||
|  | e0c4829ec6 | ||
|  | 5af1575329 | ||
|  | 884f966b86 | ||
|  | f6c6fbc223 | ||
|  | b0cc396bca | ||
|  | ae463518f6 | ||
|  | 2be2e9a0d8 | ||
|  | e405fddf74 | ||
|  | c269b0dd91 | ||
|  | 8c3211263a | ||
|  | bf04e7c089 | ||
|  | c7c6e48b1a | ||
|  | 974ca773be | ||
|  | 9270c2df19 | ||
|  | b39ff92f34 | ||
|  | 7454167f78 | ||
|  | 5ceb3a962f | ||
|  | 52bd5642da | ||
|  | c39c93725f | ||
|  | d00f0b9fa7 | ||
|  | 01cfc70982 | ||
|  | e6aec189bd | ||
|  | c98fff1647 | ||
|  | 0009e31bd3 | ||
|  | db95e880b2 | ||
|  | e69fea4a59 | ||
|  | 4360800a6e | ||
|  | b179e2b031 | ||
|  | ecdec75b4e | ||
|  | 5cb2e33353 | ||
|  | 43ff2e531a | ||
|  | 1c2c9db8f0 | ||
|  | 7ea183baef | ||
|  | ab87fac6d8 | ||
|  | 1e3b7eee3b | ||
|  | 4de028fc3b | ||
|  | 604e5dfaaf | ||
|  | 05e0c2ec9e | ||
|  | 76bd005bdc | ||
|  | 5effaed352 | ||
|  | cedaf4809f | ||
|  | 6deaf5c268 | ||
|  | 9dc6a26472 | ||
|  | 14ad5916fc | ||
|  | 1a46738649 | ||
|  | 9e5e3b099a | ||
|  | 292ce75cc2 | ||
|  | ce7df7afd4 | ||
|  | e28e793f81 | ||
|  | 3e561976db | ||
|  | 273a4eb7d0 | ||
|  | 6175f85bb6 | ||
|  | a80579f63a | ||
|  | 96d6bcf26e | ||
|  | 49e8df25ac | ||
|  | 6a05850f21 | ||
|  | 5e7c3defe3 | ||
|  | 6c0987d4d0 | ||
|  | 6eba9feffe | ||
|  | 8adfcf5950 | ||
|  | 36d6fa512a | ||
|  | 79b6e9b393 | ||
|  | dc2e2cbd4b | ||
|  | 5c12dac30f | ||
|  | 641929191e | ||
|  | 617321631a | ||
|  | ddc0c899f8 | ||
|  | cdec42c1ae | ||
|  | c48f469e39 | ||
|  | 44909cc7b8 | ||
|  | 8f61e1568c | ||
|  | b7be7a0fd8 | ||
|  | 1526a4e084 | ||
|  | dbdb9574b1 | ||
|  | 853ae6386c | ||
|  | a4b56c74c7 | ||
|  | d7f1951e44 | ||
|  | 7e2ff9825e | ||
|  | 9b423396ec | ||
|  | 781146b2fb | ||
|  | 84937d1ce0 | ||
|  | 98cce66aa4 | ||
|  | 043c2d4858 | ||
|  | 99cc434779 | ||
|  | 5095d17e81 | ||
|  | 87d835ae37 | ||
|  | 6939ca768b | ||
|  | e3957e8239 | ||
|  | 4ad6e45216 | ||
|  | 76e5eeea3f | ||
|  | eb17f57761 | ||
|  | b0db14d8b0 | ||
|  | 2b644fa81b | ||
|  | 190ccee820 | ||
|  | 4e7dd32e78 | ||
|  | 5817fb66ae | ||
|  | 9cb04eef93 | ||
|  | 0019fe7f04 | ||
|  | 852c6f2de1 | ||
|  | c4191de2e7 | ||
|  | 4de61defc9 | ||
|  | 0aa88590d0 | ||
|  | 405f3ee5fe | ||
|  | bc339f774a | ||
|  | e67b695b23 | ||
|  | 4a7633ab99 | ||
|  | c58f2ef61f | ||
|  | 3866e6a3f2 | ||
|  | 381686fc66 | ||
|  | a918c285bf | ||
|  | 1e20eafbe0 | ||
|  | 39399934ee | ||
|  | b47635150a | ||
|  | 78d2f69ed5 | ||
|  | 7a98dc669e | ||
|  | 2f15bb5085 | ||
|  | 712a578e6c | ||
|  | d8dfc4ccb2 | ||
|  | e413007eb0 | ||
|  | 6d1d3e48d8 | ||
|  | 04966164ce | ||
|  | 8b62aa7cc7 | ||
|  | 1088e8c6a5 | ||
|  | 8c54c2226f | ||
|  | f74ac1f18b | ||
|  | 25931e62fd | ||
|  | 707a940399 | ||
|  | 87ef50d384 | ||
|  | dcadf2b11c | ||
|  | 37a690a4c3 | ||
|  | 87ad23fb93 | ||
|  | 5f54d534e3 | ||
|  | aecae552a4 | ||
|  | eaa6b3d0be | ||
|  | c2ace91e52 | ||
|  | 0bac87c36f | ||
|  | e650d05939 | ||
|  | 85a96e4446 | ||
|  | 2569005139 | ||
|  | c50cb66aef | ||
|  | d4c5fca15b | ||
|  | 75cea4f684 | ||
|  | 68c6794d33 | ||
|  | 82f98dd54d | ||
|  | 741d781c18 | ||
|  | 0be1e43451 | ||
|  | 5366bf22bb | ||
|  | bcd91b1809 | ||
|  | 9bd5738e6f | ||
|  | bab4aa4c0a | ||
|  | e965b9b9e2 | ||
|  | 31101427d3 | ||
|  | a083dc36ba | ||
|  | 9b7b9262aa | ||
|  | 660011fa6e | ||
|  | ead31b6823 | ||
|  | 4310580cd4 | ||
|  | b005acbfda | ||
|  | 460709e6f3 | ||
|  | a8768d05a9 | ||
|  | f8e3e87a52 | ||
|  | 70f1642d0d | ||
|  | 3fc7561da4 | ||
|  | 9065226c3d | ||
|  | b7e321fa47 | ||
|  | 664665b86b | ||
|  | f4f362b7a4 | ||
|  | 577d23f460 | ||
|  | 504e168486 | ||
|  | f2f9640371 | ||
|  | ee46f832b1 | ||
|  | b0e755d410 | ||
|  | cfd24604d5 | ||
|  | 264894e595 | ||
|  | 5bb9f56247 | ||
|  | 18942ed066 | ||
|  | 85321a6f31 | ||
|  | baf641396d | ||
|  | 17c91e7014 | ||
|  | 010770684d | ||
|  | b4c503657b | ||
|  | 71bd306268 | ||
|  | dd7fab1352 | ||
|  | dacca18863 | ||
|  | 53d92cc0a6 | ||
|  | 434823f6f0 | ||
|  | 2cb1f50370 | ||
|  | 03f53f6392 | ||
|  | a70ecd7af0 | ||
|  | 8b81e58205 | ||
|  | 4500c04edf | ||
|  | 6222ddd720 | ||
|  | 8a7135cf41 | ||
|  | b4c7282956 | ||
|  | 8491a40a04 | ||
|  | 343d38b693 | ||
|  | 6cf53d7364 | ||
|  | b070d44de7 | ||
|  | 79aa40fdea | ||
|  | dcaff2785f | ||
|  | 497f5b4307 | ||
|  | be32ad0da6 | ||
|  | 8ee2bf810b | ||
|  | 28232656a9 | ||
|  | fbc2424e8f | ||
|  | 94cd13e8b8 | ||
|  | 447ed5ab37 | ||
|  | af59808611 | ||
|  | e3406a9f86 | ||
|  | 7fd1d6a4e8 | ||
|  | 0ab2a665de | ||
|  | 3895575bc2 | ||
|  | 138c2bbcbb | ||
|  | bc7af1d1c8 | ||
|  | 19cd96e392 | ||
|  | db194ab519 | ||
|  | 02ad4bfab2 | ||
|  | 56b73dcc8a | ||
|  | 7704b9c8a2 | ||
|  | 999b7ae919 | ||
|  | 252b5a88b1 | ||
|  | 01e2681a07 | ||
|  | aa32f30202 | ||
|  | 195eb53995 | ||
|  | 06fa78f54a | ||
|  | 7a57c9dbf1 | ||
|  | bb657bfa85 | ||
|  | 87181726b0 | ||
|  | f1477a1c14 | ||
|  | 4f94a9e38b | ||
|  | fbed322d3b | ||
|  | 9b0f519e4e | ||
|  | 6cd6dadd06 | ||
|  | 9a28afcb48 | ||
|  | 45b701801d | ||
|  | 062246fb12 | ||
|  | 416ebfdd68 | ||
|  | 731eb92f33 | ||
|  | dbe2aec79c | ||
|  | cd9cafe3a1 | ||
|  | 067cc23346 | ||
|  | c573a780e9 | ||
|  | 8ef4a0aa71 | ||
|  | 89ba12065c | ||
|  | 99efc290df | ||
|  | 2fbdc0a85e | ||
|  | 4242422898 | ||
|  | 008d9b1834 | ||
|  | 7c76d08958 | ||
|  | 89c9f45fd0 | ||
|  | f107497a94 | ||
|  | b5dcf30e53 | ||
|  | 0cef062084 | ||
|  | 5c30148be4 | ||
|  | 3a800585bc | ||
|  | 29c212a60e | ||
|  | 2997baa7cb | ||
|  | dc6bde594d | ||
|  | e357aa546c | ||
|  | d3fe19c5aa | ||
|  | bd24bf9bae | ||
|  | ee141544aa | ||
|  | db6f6e6a23 | ||
|  | c7d950dd5e | ||
|  | 6a96c62fde | ||
|  | 36dc8cd686 | ||
|  | 7622601a77 | ||
|  | cfd41fcf41 | ||
|  | f39e370e2a | ||
|  | c1315a3b39 | ||
|  | 53b32f97e8 | ||
|  | 6c962ec7d3 | ||
|  | 6bc1bc542f | ||
|  | f0e78a6826 | ||
|  | e53531a9fb | ||
|  | 5cd9d11329 | ||
|  | 5a3e504ec4 | ||
|  | d6e09c3880 | ||
|  | 04f44c3c7c | ||
|  | ec587423e8 | ||
|  | f57b31146d | ||
|  | 35175fd685 | ||
|  | d326ba9723 | ||
|  | ab655a56af | ||
|  | d1eb113ea8 | ||
|  | 74effa9b8d | ||
|  | bba4b1c663 | ||
|  | 8709d4dba0 | ||
|  | 4ad4657774 | ||
|  | 5abe0c955c | ||
|  | 0cedaf4fa9 | ||
|  | 0aa7d12704 | ||
|  | a234aa1f7e | ||
|  | 9f68287846 | ||
|  | cd2513ec16 | ||
|  | 91d132c2b4 | ||
|  | 97ff0ebd06 | ||
|  | 8829f56d4c | ||
|  | 37c1cab726 | ||
|  | b3eb117e87 | ||
|  | fc0a941508 | ||
|  | c72753c5da | ||
|  | e442cb677a | ||
|  | 450121eac9 | ||
|  | b2ab8f971e | ||
|  | e9c6268568 | ||
|  | 2170ee8da4 | ||
|  | 357e7333cc | ||
|  | 8bb4f02601 | ||
|  | 4213efc7a6 | ||
|  | 67a744c3e8 | ||
|  | 98818e7d63 | ||
|  | 8650ce1295 | ||
|  | 9638267b4c | ||
|  | 304e053155 | ||
|  | 89d1f52235 | ||
|  | 3312c6f5bd | ||
|  | d4ba644d07 | ||
|  | b9a504fd3a | ||
|  | cebac523dc | ||
|  | c2f4090318 | ||
|  | d562956809 | ||
|  | 62499f9b71 | ||
|  | 89cf7608f9 | ||
|  | dd26b8f183 | ||
|  | 79303dac6d | ||
|  | 4203fc161b | ||
|  | f8a31cc24f | ||
|  | fc5bfe81a0 | ||
|  | aae14de796 | ||
|  | 54e1c8d261 | ||
|  | a0cc4ca4b7 | ||
|  | 2701108c5b | ||
|  | 73bd2df2c6 | ||
|  | 0063021012 | ||
|  | 1c3e4750b3 | ||
|  | edad3246e0 | ||
|  | 3411b0993f | ||
|  | 097b5609dc | ||
|  | a42af7655e | ||
|  | 69f78b86af | ||
|  | 5f60c509c6 | ||
|  | 75e5e53276 | ||
|  | 4b2b4ed52d | ||
|  | fb21bfd6d6 | ||
|  | f14369e038 | ||
|  | ff04b72f62 | ||
|  | 4535a81617 | ||
|  | cce57b700b | ||
|  | 5b6194d131 | ||
|  | 2701238cea | ||
|  | 835f8a20e6 | ||
|  | f3a501db30 | ||
|  | 4bcd30da6b | ||
|  | 947dbb6f8a | ||
|  | 1c2fedd2bf | ||
|  | 32e826efbc | ||
|  | 138b932c6a | ||
|  | 6da2f53aad | ||
|  | 20eeacaac3 | ||
|  | 81d896be9f | ||
|  | c003dfab03 | ||
|  | 20c6b82bec | ||
|  | 046b494b53 | ||
|  | f0e98d6e0d | ||
|  | fe57321853 | ||
|  | 8510804e57 | ||
|  | acd32abac5 | ||
|  | 2b47c96cf2 | ||
|  | 1027378bda | ||
|  | e979d30659 | ||
|  | 574db704cc | ||
|  | fdb969ea89 | ||
|  | 08977854b3 | ||
|  | cecac64b68 | ||
|  | 7dabdade2a | ||
|  | e788f098e2 | ||
|  | 69406d4344 | ||
|  | d16dd26c65 | ||
|  | 12219c1bea | ||
|  | 118bdcc26e | ||
|  | 78fa96f0f4 | ||
|  | c7deb63a04 | ||
|  | 4f811eb9e9 | ||
|  | 0b265bd673 | ||
|  | ee67fabbeb | ||
|  | b213de7e62 | ||
|  | 7c01505750 | ||
|  | ae28dfd020 | ||
|  | 2a5a4e785f | ||
|  | d8bddede6a | ||
|  | b8a93e74bf | ||
|  | e60ec94d35 | ||
|  | 84af5fd0a3 | ||
|  | dbb3edec77 | ||
|  | d284b46a3e | ||
|  | 9fcb4d222b | ||
|  | d0bb1ad141 | ||
|  | b299aaed93 | ||
|  | abb3224cc5 | ||
|  | 1c66d06702 | ||
|  | e00e80ae39 | ||
|  | 4f4f106c48 | ||
|  | a286cc9d55 | ||
|  | 53bb1c719b | ||
|  | 98d5aa17e2 | ||
|  | aaaa80e4b8 | ||
|  | e70e926a40 | ||
|  | e80c1f6d59 | ||
|  | 24de360325 | ||
|  | e0039bc1e6 | ||
|  | ae5c4a0109 | ||
|  | 1d367a0da0 | ||
|  | d285f7ee4a | ||
|  | 37c84021a2 | ||
|  | 8ee9de4291 | ||
|  | 249b63453b | ||
|  | 1c0017d763 | ||
|  | df51e23639 | ||
|  | 32e71a43b8 | ||
|  | 47a1e6ddfa | ||
|  | c5f41457bb | ||
|  | f1e0c44bdd | ||
|  | 9d2e390b6a | ||
|  | 75a58b435d | ||
|  | f5474d34ac | ||
|  | c962d2544f | ||
|  | 0b87a4a810 | ||
|  | 1882afb8b6 | ||
|  | 2270c8737a | ||
|  | d6794955a4 | ||
|  | f5520f45ef | ||
|  | 9401b5ae13 | ||
|  | df64a62a03 | ||
|  | 09cea66aa8 | ||
|  | 13cc33e0a5 | ||
|  | ab36c8c9de | ||
|  | f85d4ce82f | ||
|  | 6bec4c28ba | ||
|  | fad1449259 | ||
|  | 86b3b57137 | ||
|  | b235037dd3 | ||
|  | 3108139d51 | ||
|  | 2ae99ecfa0 | ||
|  | e8ab53c270 | ||
|  | 5e9bc1127d | ||
|  | 415e61c3c9 | ||
|  | 5152f37ec8 | ||
|  | 0dbeb010cf | ||
|  | 17c465bed7 | ||
|  | add04478e5 | ||
|  | 6db72d7166 | ||
|  | 868103a9c5 | ||
|  | 0f37718671 | ||
|  | fa1445df86 | ||
|  | a783e7071e | ||
|  | a9919df5af | ||
|  | b0af31ac35 | ||
|  | c4c964a685 | ||
|  | 348ec71398 | ||
|  | a257ccc8b3 | ||
|  | fcc4296040 | ||
|  | 1684d05d49 | ||
|  | 0006f933a2 | ||
|  | 0484f97c9c | ||
|  | e430b2567a | ||
|  | fbc8ee15da | ||
|  | 68a9c05947 | ||
|  | 0a81aba899 | ||
|  | d2ae822e15 | ||
|  | fac4b08526 | ||
|  | 3a7b43c663 | ||
|  | 8fcb2d1554 | ||
|  | 590c763659 | ||
|  | 11d1267f8c | ||
|  | 8f5bae95ce | ||
|  | e6b12ef14c | ||
|  | b65674618b | ||
|  | 20dca2bea5 | ||
|  | 059e93cdcf | ||
|  | 635ab25013 | ||
|  | 995cd10df8 | ||
|  | 50f3820a6d | ||
|  | 617f3ea861 | ||
|  | 788db47b95 | ||
|  | 5fa8aaabb9 | ||
|  | 89d1af7f33 | ||
|  | 799cf27c5d | ||
|  | c930d8f773 | ||
|  | a7f921abb9 | ||
|  | bc6234e032 | ||
|  | 558bfa4e1e | ||
|  | 5d19f23372 | ||
|  | 27f08cdbfa | ||
|  | 993213e2c0 | ||
|  | 49470c05fa | ||
|  | ee0a060b79 | ||
|  | 500e3157b9 | ||
|  | eba86b1d23 | ||
|  | b69a563fc2 | ||
|  | a900c36395 | ||
|  | 1d9b324d3e | ||
|  | 539e7b8efe | ||
|  | 50a477ee47 | ||
|  | 7000123a8b | ||
|  | d48a7d2398 | ||
|  | 389a00ce59 | ||
|  | 7a460de3c2 | ||
|  | 8ea1f4a751 | ||
|  | 1c69ccc6cd | ||
|  | 84b5bbd3b6 | ||
|  | 9ccd327298 | ||
|  | 11df36f3cf | ||
|  | f62dd0e3cc | ||
|  | ad18b6e15e | ||
|  | c00b80ca29 | ||
|  | 92ed4ba3f8 | ||
|  | 7de9775dd9 | ||
|  | 5ce9060e5c | ||
|  | f727d5cb5a | ||
|  | 4735fb1ebb | ||
|  | c7d05cc13d | ||
|  | 51c152ff4a | ||
|  | eeed2a840c | ||
|  | 4aaa111925 | ||
|  | e31248f018 | ||
|  | 8b4cf022f2 | ||
|  | 4e7455268a | ||
|  | 680f8ae814 | ||
|  | 90555a4cea | ||
|  | 56a62db591 | ||
|  | cf51997680 | ||
|  | f05cc18d61 | ||
|  | 5384c2e0f5 | ||
|  | 9bfbf80a0e | ||
|  | f874d7754f | ||
|  | a669f79480 | ||
|  | 1c3894743a | ||
|  | 75cdf17df4 | ||
|  | de7dd1e60a | ||
|  | 0ee574a718 | ||
|  | faac894706 | ||
|  | dac2fad48e | ||
|  | 77f624b01e | ||
|  | e24ffebfc8 | ||
|  | 70d07d1609 | ||
|  | bfb3303d87 | ||
|  | 660705a436 | ||
|  | 74a3f97671 | ||
|  | b3e35bb494 | ||
|  | 76adac7c72 | ||
|  | 5dc75ebb67 | ||
|  | d686ce12b6 | ||
|  | d3c40a423e | ||
|  | 2fb1e6dab8 | ||
|  | 10430b347f | ||
|  | e0e3f6ac3e | ||
|  | c694cbffdc | ||
|  | bdd0e5d771 | ||
|  | aa98e427f0 | ||
|  | daa6f4c94c | ||
|  | 4a76663fb2 | ||
|  | cebda5028a | ||
|  | 3fa377a580 | ||
|  | a11c1005a8 | ||
|  | 4a6aea9328 | ||
|  | 4ca041e93e | ||
|  | 52a866a405 | ||
|  | 8b6bd0e6ac | ||
|  | 780fc4639a | ||
|  | 3692fc9d83 | ||
|  | c2a0b1b4c6 | ||
|  | 21bbdb5419 | ||
|  | aa1c08962c | ||
|  | 8a5d0399dd | ||
|  | f2cd0b0c4a | ||
|  | c2b66bbe73 | ||
|  | 48b957f1d5 | ||
|  | 3683984c8d | ||
|  | a3431512d8 | ||
|  | d832b787e7 | ||
|  | 6f75b02723 | ||
|  | b8241710bd | ||
|  | d638404b6a | ||
|  | 9362ca3ed9 | ||
|  | d1a03c6d17 | ||
|  | c6c31702c2 | ||
|  | bd2d88c96e | ||
|  | 76b1857e4e | ||
|  | 095bd17d10 | ||
|  | 204bfac3fa | ||
|  | ac49b0ca93 | ||
|  | c5b04f6fef | ||
|  | 5c58fda46d | ||
|  | 062730c70c | ||
|  | cade1990ce | ||
|  | 59b6e61816 | ||
|  | daff7ff158 | ||
|  | 0862860961 | ||
|  | 1cb24045a0 | ||
|  | 622358b172 | ||
|  | 7998884a9d | ||
|  | 51ddecd101 | ||
|  | 7a35ab1d1e | ||
|  | 48564ba52a | ||
|  | 49efffd740 | ||
|  | d6ac224c8f | ||
|  | a772b8c3f2 | ||
|  | b580953dcd | ||
|  | d86653c763 | ||
|  | dded4fca76 | ||
|  | 36365ffa6b | ||
|  | 0f9aeeaa27 | ||
|  | d8ebcd0ef7 | ||
|  | 6e445487b1 | ||
|  | 6605e461c7 | ||
|  | 40ce4e2275 | ||
|  | 8fef9e363e | ||
|  | 4792c2770d | ||
|  | 87bb49da36 | ||
|  | 1c0071d9ce | ||
|  | efded35c2e | ||
|  | 1d74240b9a | ||
|  | 098184ff7b | ||
|  | 4083533916 | ||
|  | feb1acd43a | ||
|  | a9591db734 | ||
|  | 9ebf148cbe | ||
|  | a473e5e19a | ||
|  | 5d3034c231 | ||
|  | c3a895af64 | ||
|  | cea5aecbf2 | ||
|  | 0e61e70670 | ||
|  | 1e333c0939 | ||
|  | 917b6ec03c | ||
|  | fe67c52ead | ||
|  | 909c7bee3e | ||
|  | 27ca54d138 | ||
|  | 2147c3a646 | ||
|  | a99120116f | ||
|  | 802efeaff2 | ||
|  | 9ad3af1ef6 | ||
|  | 715727b811 | ||
|  | c6eaa7b836 | ||
|  | c2fceea2a5 | ||
|  | 190e11f7ea | ||
|  | ad7413a5ff | ||
|  | 903b9e627a | ||
|  | c5c1e96cf8 | ||
|  | 62fbb04c9d | ||
|  | 728dc62d0b | ||
|  | 2dfe1b1c6b | ||
|  | 35d4a1a6af | ||
|  | eb3fa5aa6b | ||
|  | 438384425a | ||
|  | 0b6f102436 | ||
|  | c9b7ec72d8 | ||
|  | 256c7f1789 | ||
|  | 4e5a323c62 | ||
|  | f4a3bbd237 | ||
|  | fe73f2d579 | ||
|  | f79fcc7073 | ||
|  | 4c4b3790c7 | ||
|  | bd60b464bb | ||
|  | 6bce852765 | ||
|  | 3b19a5a59d | ||
|  | f024583011 | ||
|  | 1111baacb2 | ||
|  | 1b9c913efb | ||
|  | 3524c36e1b | ||
|  | cf87cea9f8 | ||
|  | bfa34404b8 | ||
|  | 0aba5f35bf | ||
|  | 663bc0842a | ||
|  | 7d10c96e73 | ||
|  | 6b2720fab0 | ||
|  | e74ad5132a | ||
|  | 1f6f89c1fd | ||
|  | 4d55e60980 | ||
|  | ddaaccd5af | ||
|  | c20b7dac3d | ||
|  | 1f779d5094 | ||
|  | 715401ca8e | ||
|  | e7cd922d8b | ||
|  | 187feee0c1 | ||
|  | 49e962a7dc | ||
|  | 633ff601e5 | ||
|  | 331cf37054 | ||
|  | 23e4b9002f | ||
|  | c0de3c8053 | ||
|  | a82a3b084a | ||
|  | 67c298e66b | ||
|  | c110ccb9ae | ||
|  | 0143380306 | ||
|  | af9000d3c8 | ||
|  | 097d798e5e | ||
|  | 1d9f9f221a | ||
|  | 214a367f48 | ||
|  | 2fb46551a2 | ||
|  | 6bcf330ae0 | ||
|  | 2075a8b18c | ||
|  | 1275ac6c42 | ||
|  | 708f20b7af | ||
|  | a2c0c708e8 | ||
|  | 2f2c65d91e | ||
|  | cd5fcc7ca7 | ||
|  | aa29e7be48 | ||
|  | 93febe34b0 | ||
|  | f086e6d3c1 | ||
|  | 22e51e1c96 | ||
|  | 63a5336f31 | ||
|  | bfc6c53cc5 | ||
|  | 236017f310 | ||
|  | 0a1d9b4dfd | ||
|  | b50d090946 | ||
|  | 00b5db52cf | ||
|  | 24cb30e2c5 | ||
|  | 4549145ab5 | ||
|  | 67b0217754 | ||
|  | ccae9efdf0 | ||
|  | 59d596b222 | ||
|  | 4878eb2c45 | ||
|  | 7755392f57 | ||
|  | dc2ea20959 | ||
|  | 8eaea2bd17 | ||
|  | 58e559918f | ||
|  | f38a3fca5b | ||
|  | 1ea145b384 | ||
|  | 0d9567575a | ||
|  | e82f176289 | ||
|  | d4b51c040e | ||
|  | 125d0efbd8 | ||
|  | 3215afc504 | ||
|  | c73ff3ce1b | ||
|  | f9c159a051 | ||
|  | 2ab1325c90 | ||
|  | 5b0f7ff506 | ||
|  | 9269bc84f2 | ||
|  | 4e8b651e18 | ||
|  | 65b4f79534 | ||
|  | 5dd43dbc45 | ||
|  | 5f73074c7e | ||
|  | f5d6ba27b2 | ||
|  | 73fa70b41f | ||
|  | 2a1cda42e7 | ||
|  | 1bd7e31466 | ||
|  | eb49e1fb4a | ||
|  | 9838c2f0ce | ||
|  | 6041df8370 | ||
|  | 2933dce3ef | ||
|  | dab377d37b | ||
|  | f35e41baf1 | ||
|  | c4083a2942 | ||
|  | 36c20bbe53 | ||
|  | e34634f5af | ||
|  | cba9e5b669 | ||
|  | 1f3c46a6b0 | ||
|  | 799a5ffa47 | ||
|  | b000707c10 | ||
|  | feba4de1d6 | ||
|  | 951fdb27ca | ||
|  | 9697fb3d84 | ||
|  | 2dbed4500a | ||
|  | fd9d0e433d | ||
|  | f096f3ef81 | ||
|  | cc4a063695 | ||
|  | b64cabc3c9 | ||
|  | 3dd460717c | ||
|  | bf658a522b | ||
|  | e9be7e712d | ||
|  | e40cd2a809 | ||
|  | dbabeb9692 | ||
|  | 8dd37d76b0 | ||
|  | fd475aa358 | ||
|  | f0988c0e32 | ||
|  | 0632f09bff | ||
|  | ba599aaca0 | ||
|  | ff05919e89 | ||
|  | 52e63fa101 | ||
|  | 96ceccd12a | ||
|  | 87994fe006 | ||
|  | fa12c81a03 | ||
|  | 344ce63455 | ||
|  | ec4daacf9e | ||
|  | f3e8308718 | ||
|  | 515ac5d941 | ||
|  | 954c7e7e50 | ||
|  | 67ff57f3a3 | ||
|  | c10c70c1e5 | ||
|  | 04592a98d2 | ||
|  | c9c4aac6cf | ||
|  | 8b2c7586ce | ||
|  | 32e22dfe84 | ||
|  | d70b885722 | ||
|  | ac6c4b13f5 | ||
|  | ececdad22d | ||
|  | bf659781b0 | ||
|  | 2c6bb195a4 | ||
|  | c032cd08b3 | ||
|  | 39e7a7a231 | ||
|  | 6e14cd2c39 | ||
|  | aab3baaea7 | ||
|  | b8453c3b4f | ||
|  | 6ce0e2cd5b | ||
|  | 76beaae7f2 | ||
|  | c1a7f9edbe | ||
|  | b5f2fe2f0a | ||
|  | 98a90d49cb | ||
|  | f55e982cb5 | ||
|  | 686c7defeb | ||
|  | 0b1e483c53 | ||
|  | 457d7df129 | ||
|  | ce776a547c | ||
|  | ded0567cbf | ||
|  | c9cac83d09 | ||
|  | 4fbe6b01a8 | ||
|  | ee9585264e | ||
|  | c9ffead7bf | ||
|  | ed69d42005 | ||
|  | 0b47ee306b | ||
|  | e4e63619d4 | ||
|  | f32cca292a | ||
|  | e87ea19ff1 | ||
|  | 0214793740 | ||
|  | fc9dd5d743 | ||
|  | 9e6d5dd2b9 | ||
|  | bdad197e2c | ||
|  | 7e139288a6 | ||
|  | 6e7935abaf | ||
|  | 3ba0cc20f1 | ||
|  | dd28de1796 | ||
|  | 9eecc9e19a | ||
|  | 6530cb6b05 | ||
|  | 41ce613379 | ||
|  | 5e2785caba | ||
|  | d7cc000976 | ||
|  | 50d8ff95ae | ||
|  | b2de1459b6 | ||
|  | f0ffbea0b2 | ||
|  | 199ccca0fe | ||
|  | 1d9b355743 | ||
|  | f0437fbb07 | ||
|  | abc404a5b7 | ||
|  | 04b9e21330 | ||
|  | 1044aa071b | ||
|  | 4c3192c8cc | ||
|  | 689e77a025 | ||
|  | 3bd89403d2 | ||
|  | b4800d9bcb | ||
|  | 05485e8539 | ||
|  | 0e03dc0868 | ||
|  | 352b1ed10a | ||
|  | 0db1244d04 | ||
|  | ece08b8179 | ||
|  | b8945ae233 | ||
|  | dcaf7b0a20 | ||
|  | f982cdc178 | ||
|  | b265e59834 | ||
|  | 4a843a6624 | ||
|  | 241ef5b99d | ||
|  | f39f575a9c | ||
|  | 1521307f1e | ||
|  | dd122111e6 | ||
|  | 00c177fa74 | ||
|  | f6c7e49eb8 | ||
|  | 1a8dc3d18a | ||
|  | 38a163a09a | ||
|  | 8f031246d2 | ||
|  | 8f3d97dde7 | ||
|  | 4acaf24d65 | ||
|  | 9a8dbbbcf8 | ||
|  | a3efc4c726 | ||
|  | 0278bf328f | ||
|  | 17ddd96cc6 | ||
|  | 0e82e79aea | ||
|  | 30f124c061 | ||
|  | e19d90fcfc | ||
|  | 184bbdd23d | ||
|  | 30b50aec95 | ||
|  | c3c3d81db1 | ||
|  | 49b7231283 | ||
|  | edbedcdad3 | ||
|  | e4ae5f74e6 | ||
|  | 2c7ffe08d7 | ||
|  | 3ca46bae46 | ||
|  | 7e82aaf843 | ||
|  | 315bd71adf | ||
|  | 2c612c9aeb | ||
|  | 36aee085f7 | ||
|  | d01bb69a9c | ||
|  | c9b1c48c72 | ||
|  | aea3843cf2 | ||
|  | 131b6f4b9a | ||
|  | 6efb8b735a | ||
|  | 223b7af2ce | ||
|  | e72c2a6982 | ||
|  | dd9b93970e | ||
|  | e4c7cd81a9 | ||
|  | 12b3a62586 | ||
|  | 2da3bdcd47 | ||
|  | c1dccbe0ba | ||
|  | 9629fcde68 | ||
|  | cae436b566 | ||
|  | 01714700ae | ||
|  | 51e6c4852b | ||
|  | b206c5d64e | ||
|  | 62c3272351 | ||
|  | c5d822c70a | ||
|  | 9c09b4061a | ||
|  | c26fb43ced | ||
|  | deb8f20db6 | ||
|  | 50e18ed8ff | ||
|  | 31f3895f40 | ||
|  | 615929268a | ||
|  | b8b15814cf | ||
|  | 7766fffe83 | ||
|  | 2a16c150d1 | ||
|  | 418c2166cc | ||
|  | a4dd44f648 | ||
|  | 5352f7cda7 | ||
|  | 5533b47099 | ||
|  | e9b14464ee | ||
|  | 4e986e5cd1 | ||
|  | 8a59b40c53 | ||
|  | 391caca043 | ||
|  | 171ce348d6 | ||
|  | c2cc729135 | ||
|  | e7e71b76f0 | ||
|  | a2af61cf6f | ||
|  | e111edd5e4 | ||
|  | 3375377371 | ||
|  | 0ced020c67 | ||
|  | c0d7aa9e4a | ||
|  | e5b3d2a312 | ||
|  | 7b4a794981 | ||
|  | 86a859de17 | ||
|  | b3aaa7bd0f | ||
|  | a90586e6a8 | ||
|  | 807f272895 | ||
|  | f050647b43 | ||
|  | 73baebbd16 | ||
|  | f327f698b9 | ||
|  | 8164910fe8 | ||
|  | 3498644055 | ||
|  | d31116b54c | ||
|  | aced110cdf | ||
|  | e9ab6aec77 | ||
|  | 15b261c861 | ||
|  | 970badce66 | ||
|  | 64304a9d65 | ||
|  | d1983553d2 | ||
|  | 6b15df3bcd | ||
|  | 730b1fff71 | ||
|  | c3add751e5 | ||
|  | 9da2dbdc1c | ||
|  | 977f09c470 | ||
|  | 4d0c6a8802 | ||
|  | 5345565037 | ||
|  | be38c27c64 | ||
|  | 82a0401099 | ||
|  | 33bea1b663 | ||
|  | f083acd46d | ||
|  | 5aacd15272 | ||
|  | cb7674b091 | ||
|  | 3899c7ad56 | ||
|  | d2debced09 | ||
|  | b86c0ddc48 | ||
|  | ba36f33bd8 | ||
|  | 49368a10ba | ||
|  | ac1568cacf | ||
|  | 862ca3439d | ||
|  | fdd4f9f2aa | ||
|  | aa2dc49ebe | ||
|  | cc23b7ee74 | ||
|  | f6f9fc5a45 | ||
|  | 26c8589399 | ||
|  | c2469935cb | ||
|  | 5e7c20955e | ||
|  | 967fa38108 | ||
|  | 280fe8e36b | ||
|  | 03ca96ccc3 | ||
|  | b5b8a2c9d5 | ||
|  | 0008832730 | ||
|  | c9b385db4b | ||
|  | c951b66ae0 | ||
|  | de735f3a45 | ||
|  | 19161425f3 | ||
|  | c69e8d5bf4 | ||
|  | 3d3bce2788 | ||
|  | 1cb0dc7f8e | ||
|  | cd5c56e601 | ||
|  | 8c979905e4 | ||
|  | 4d69f15f48 | ||
|  | 083f6572f7 | ||
|  | 4e7dd75266 | ||
|  | 3eb83f449b | ||
|  | d31f69117b | ||
|  | f5f9e3ac97 | ||
|  | 598d6c598c | ||
|  | 744727087a | ||
|  | f93212a665 | ||
|  | 6dade82d2c | ||
|  | 6b737bf1d7 | ||
|  | 94dbd70677 | ||
|  | 527ae0348e | ||
|  | 79629c430a | ||
|  | 908dd61be5 | ||
|  | 88f77b8cca | ||
|  | 1e846657d1 | ||
|  | ce70f62a88 | ||
|  | bca0cdbb62 | ||
|  | 1ee11e04e6 | ||
|  | 6eef44f212 | ||
|  | 8bd94f4a1c | ||
|  | 4bc4701372 | ||
|  | dfd89b503a | ||
|  | 060dc54832 | ||
|  | f7a4ea5793 | ||
|  | 71b478e6e2 | ||
|  | ed8fff8c52 | ||
|  | 95dc78db10 | ||
|  | addeac64c7 | ||
|  | d77ec22007 | ||
|  | 20030c91b7 | ||
|  | 8b366e255c | ||
|  | 6da366fcb0 | ||
|  | 2fa35f851e | ||
|  | e4ca4260bb | ||
|  | b69aace8d8 | ||
|  | 79097bb43c | ||
|  | 806fac1742 | ||
|  | 4f97d7cf8d | ||
|  | 42acc457af | ||
|  | c02920607f | ||
|  | 452885c271 | ||
|  | 5c242a07b6 | ||
|  | 088899d59f | ||
|  | 1faff2a37e | ||
|  | 23c8d3d045 | ||
|  | a033388d2b | ||
|  | 82fe45ac56 | ||
|  | bcb7fcda6b | ||
|  | 726a98100b | ||
|  | 2f021a0c2b | ||
|  | eb05cb6c6e | ||
|  | 7530af95da | ||
|  | 8399e95bda | ||
|  | 3b4dfe326f | ||
|  | 2e787a254e | ||
|  | f888bed1a6 | ||
|  | d865e9f35a | ||
|  | fc7fe70f66 | ||
|  | 5aff39d2b2 | ||
|  | d1be37a04a | ||
|  | b0fd8bf7d4 | ||
|  | b9cf8f3973 | ||
|  | 4588f11613 | ||
|  | 1a618c3c97 | ||
|  | d500a51d97 | ||
|  | 734e9d3874 | ||
|  | bd5cfc2f1b | ||
|  | 89f88ee78c | ||
|  | b2ae14695a | ||
|  | 19d86b44d9 | ||
|  | 85be62e38b | ||
|  | 80f3d90200 | ||
|  | 0249fa6e75 | ||
|  | 2d0696e048 | ||
|  | ff32ec515e | ||
|  | a6935b0293 | ||
|  | 63eb08ba9f | ||
|  | e5b67d2b3a | ||
|  | 9e10af6885 | ||
|  | 42bc9115d2 | ||
|  | 0a569ce413 | ||
|  | 9a16639a61 | ||
|  | 57953c68c6 | ||
|  | 088d08963f | ||
|  | 7bc8196821 | ||
|  | 7715299dd3 | ||
|  | b8ac9b7994 | ||
|  | 98e7d8f728 | ||
|  | e7fd871ffe | ||
|  | 14aab62f32 | ||
|  | cb81fe962c | ||
|  | fc970d2dea | ||
|  | b0e203d1f9 | ||
|  | 37cef05b19 | ||
|  | 5886a42901 | ||
|  | 2fd99f807d | ||
|  | 3d4cbd7d10 | ||
|  | f10d03c238 | ||
|  | f9a66ffb0e | ||
|  | 777a50063d | ||
|  | 0bb9154747 | ||
|  | 30c3f45072 | ||
|  | 0d5ca67f32 | ||
|  | 4a8bf6aebd | ||
|  | b11db090d8 | ||
|  | 189391fccd | ||
|  | 86d4c43909 | ||
|  | 5994f40982 | ||
|  | 076d32dee5 | ||
|  | 16c8e38ecd | ||
|  | eacbcda8e5 | ||
|  | 59be76cd44 | ||
|  | 5bb0e7e8b3 | ||
|  | b78d207121 | ||
|  | 0fcbcdd08c | ||
|  | ed6c683922 | ||
|  | 9fe1edb02b | ||
|  | fb3811a708 | ||
|  | 18f8658eec | ||
|  | 3ead4676b0 | ||
|  | d30001d23d | ||
|  | 06bbf0d656 | ||
|  | 6ddd952e04 | ||
|  | 027ad0c3ee | ||
|  | 3abad2b87b | ||
|  | 32a1c7c5d5 | ||
|  | f06e165bd4 | ||
|  | 1c843b24f7 | ||
|  | 2ace9ed380 | ||
|  | 5f30c0ae03 | ||
|  | ef60adf7e2 | ||
|  | 7354b462e8 | ||
|  | da904d6be8 | ||
|  | c5fbbbbb5c | ||
|  | 5010387d8a | ||
|  | f00c54a7fb | ||
|  | 9f52c169d0 | ||
|  | bf18339404 | ||
|  | 2ad12b074b | ||
|  | a6788ffe8d | ||
|  | 0e884df486 | ||
|  | ef1c55286f | ||
|  | abc0424c26 | ||
|  | 44e5c82e6d | ||
|  | 5849c446ed | ||
|  | 12b7317831 | ||
|  | fe323f59af | ||
|  | a00e56f219 | ||
|  | 1a7852794f | ||
|  | 22b1373a57 | ||
|  | 17d78b1469 | ||
|  | 4d8b32b249 | ||
|  | b65bea2550 | ||
|  | 0b52ccd200 | ||
|  | 3006a07059 | ||
|  | 801dbc7a9a | ||
|  | 4f4e895fb7 | ||
|  | cc57c3b655 | ||
|  | ca6ec9c5c7 | ||
|  | 633b1f0a78 | ||
|  | 6136b9bf9c | ||
|  | 524a3ba566 | ||
|  | 58580320f9 | ||
|  | 759b0a994d | ||
|  | d2800473e4 | ||
|  | f5b1a2065e | ||
|  | 5e62532295 | ||
|  | c1bee96c40 | ||
|  | f273253a2b | ||
|  | 012bbcf770 | ||
|  | b54cb47b2e | ||
|  | 1b15f43745 | ||
|  | 96771bf1bd | ||
|  | 580078bddb | ||
|  | c5c7080ec6 | ||
|  | 408339b51d | ||
|  | 02e3d44998 | ||
|  | 156f13ded1 | ||
|  | d288467cb7 | ||
|  | 21662c9f3f | ||
|  | 9149fe6cdd | ||
|  | 9a146192b7 | ||
|  | 3a9d3b7b61 | ||
|  | f03f0973ab | ||
|  | 7ec0881e8c | ||
|  | 59e1ab42ff | ||
|  | 722216b901 | ||
|  | bd8f3dc368 | ||
|  | 33cd94a141 | ||
|  | 053ac74734 | ||
|  | cced99fafa | ||
|  | a009ff53f7 | ||
|  | ca16c4108d | ||
|  | d1b6c67dc3 | ||
|  | a61f8133d5 | ||
|  | 38d797a544 | ||
|  | 16c1877f50 | ||
|  | da5f15a778 | ||
|  | 396c64ecf7 | ||
|  | 252c3a7985 | ||
|  | a3ecbf0ae7 | ||
|  | 314327d8f2 | ||
|  | bfacd06929 | ||
|  | 4f5e8f8cf5 | ||
|  | 1fbb4c09cc | ||
|  | b332e1992b | ||
|  | 5955940b82 | ||
|  | 231a03bcfd | ||
|  | bc85723657 | ||
|  | be32b743c6 | ||
|  | 83c9843059 | ||
|  | 11cf43626d | ||
|  | a6dc5e2ce3 | ||
|  | 38593a0394 | ||
|  | 95309afeea | ||
|  | c2bf6fe2a3 | ||
|  | 99ac324fbd | ||
|  | 5562de330f | ||
|  | 95014236ac | ||
|  | 6aa7386138 | ||
|  | 3226a1f588 | ||
|  | b4cf890cd8 | ||
|  | ce09e323af | ||
|  | 941aedb177 | ||
|  | 87a0d502a3 | ||
|  | cab7c1b0b8 | ||
|  | d5892341b6 | ||
|  | 646557a43e | ||
|  | ed8d34ab43 | ||
|  | 5e34463c77 | ||
|  | 1b14eb7959 | ||
|  | ed48c2d0ed | ||
|  | 26fe84b660 | ||
|  | 5938230270 | ||
|  | 1a33a047fa | ||
|  | 43a8bcefb9 | ||
|  | 2e740e513f | ||
|  | 8a21a86b61 | ||
|  | f600116205 | ||
|  | 1c03705de8 | ||
|  | f7e461fac6 | ||
|  | 03ce6c97ff | ||
|  | ffd9e76e07 | ||
|  | fc49cb1e67 | ||
|  | f5712d9f25 | ||
|  | 161d57bdda | ||
|  | bae0d440bf | ||
|  | fff052dde1 | ||
|  | 73b06eaa02 | ||
|  | 08a8ebed17 | ||
|  | 74d07426b3 | ||
|  | 69a2bba99a | ||
|  | 4d685d78ee | ||
|  | 5845ec3f49 | ||
|  | 13373426fe | ||
|  | 8e55551a06 | ||
|  | 12a3f0ac31 | ||
|  | 18e33edc88 | ||
|  | c72c5ad4ee | ||
|  | 0fbc81ab2f | ||
|  | af0a34cf82 | ||
|  | b4590c5398 | ||
|  | f787a66230 | ||
|  | b21a99fd62 | ||
|  | eb16306cde | ||
|  | 7bc23687e3 | ||
|  | e1eaa057f2 | ||
|  | 97c264ca3e | ||
|  | cf848ab1f7 | ||
|  | cf83f9b0fd | ||
|  | d98e361083 | ||
|  | ce7f5309c7 | ||
|  | 75c485ced7 | ||
|  | 9c6e2ec012 | ||
|  | 1a02948a61 | ||
|  | 8b05ba4ba1 | ||
|  | 21e2874cb7 | ||
|  | 360ed5c46c | ||
|  | 5099bc365d | ||
|  | 12986da147 | ||
|  | 23e72797bc | ||
|  | ac7b6f8f55 | ||
|  | 981b9ff11e | ||
|  | 4186906f4c | ||
|  | 0850d24e0c | ||
|  | 7ab8334c96 | ||
|  | a4d7329ab7 | ||
|  | 3f4eae6bce | ||
|  | 518cf4be57 | ||
|  | 71096182be | ||
|  | 6452e927ea | ||
|  | bc70cfa6f0 | ||
|  | 2b6e5ebd2d | ||
|  | c761bd799a | ||
|  | 2f7c2fdee4 | ||
|  | 70a76ec343 | ||
|  | 7c3f64abf2 | ||
|  | f5f38f195c | ||
|  | 7e84f4f015 | ||
|  | 4802f8cf07 | ||
|  | cc05e67d8f | ||
|  | 2b6b174517 | ||
|  | a1d05e6e12 | ||
|  | f95ceb6a9b | ||
|  | 8f91b0726d | ||
|  | 97807f4383 | ||
|  | 5f42237f2c | ||
|  | 68289cfa54 | ||
|  | 42ea30270f | ||
|  | ebbbbf3d82 | ||
|  | 27516e2d16 | ||
|  | 84bb6f915e | ||
|  | 46752f758a | ||
|  | 34c4c22e61 | ||
|  | af2d0b8421 | ||
|  | 638b05a49a | ||
|  | 7a13e8a7fc | ||
|  | d9fa74711d | ||
|  | 41867f578f | ||
|  | 0bf41ed4ef | ||
|  | d080b4a731 | ||
|  | ca4232ada9 | ||
|  | ad348f91c9 | ||
|  | 990f915f42 | ||
|  | 53d720217b | ||
|  | 7a06ff480d | ||
|  | 3ef551f788 | ||
|  | f0125cdc36 | ||
|  | ed5f6736df | ||
|  | 15d8be0fae | ||
|  | 46f3e61360 | ||
|  | 87ad8c98d4 | ||
|  | 9bbdc4100f | ||
|  | c80307e8ff | ||
|  | c1d77e1041 | ||
|  | d9e83650dc | ||
|  | f6d635acd9 | ||
|  | 0dbd8a01ff | ||
|  | 8d755d41e0 | ||
|  | 190473bd32 | ||
|  | 030d1ec254 | ||
|  | 5a2b91a084 | ||
|  | a50a05e4e7 | ||
|  | 6cb5a87c79 | ||
|  | b9f89ca552 | ||
|  | 26c9fd5dea | ||
|  | e81a9b6fe0 | ||
|  | 452450e451 | 
							
								
								
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | --- | ||||||
|  | name: Bug report | ||||||
|  | about: Create a report to help us improve | ||||||
|  | title: '' | ||||||
|  | labels: bug | ||||||
|  | assignees: '9001' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | NOTE: | ||||||
|  | all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Describe the bug** | ||||||
|  | a description of what the bug is | ||||||
|  |  | ||||||
|  | **To Reproduce** | ||||||
|  | List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it | ||||||
|  |  | ||||||
|  | **Expected behavior** | ||||||
|  | a description of what you expected to happen | ||||||
|  |  | ||||||
|  | **Screenshots** | ||||||
|  | if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^) | ||||||
|  |  | ||||||
|  | **Server details** | ||||||
|  | if the issue is possibly on the server-side, then mention some of the following: | ||||||
|  | * server OS / version:  | ||||||
|  | * python version:  | ||||||
|  | * copyparty arguments:  | ||||||
|  | * filesystem (`lsblk -f` on linux):  | ||||||
|  |  | ||||||
|  | **Client details** | ||||||
|  | if the issue is possibly on the client-side, then mention some of the following: | ||||||
|  | * the device type and model:  | ||||||
|  | * OS version:  | ||||||
|  | * browser version:  | ||||||
|  |  | ||||||
|  | **Additional context** | ||||||
|  | any other context about the problem here | ||||||
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | --- | ||||||
|  | name: Feature request | ||||||
|  | about: Suggest an idea for this project | ||||||
|  | title: '' | ||||||
|  | labels: enhancement | ||||||
|  | assignees: '9001' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | all of the below are optional, consider them as inspiration, delete and rewrite at will | ||||||
|  |  | ||||||
|  | **is your feature request related to a problem? Please describe.** | ||||||
|  | a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]` | ||||||
|  |  | ||||||
|  | **Describe the idea / solution you'd like** | ||||||
|  | a description of what you want to happen | ||||||
|  |  | ||||||
|  | **Describe any alternatives you've considered** | ||||||
|  | a description of any alternative solutions or features you've considered | ||||||
|  |  | ||||||
|  | **Additional context** | ||||||
|  | add any other context or screenshots about the feature request here | ||||||
							
								
								
									
										10
									
								
								.github/ISSUE_TEMPLATE/something-else.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.github/ISSUE_TEMPLATE/something-else.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | --- | ||||||
|  | name: Something else | ||||||
|  | about: "┐(゚∀゚)┌" | ||||||
|  | title: '' | ||||||
|  | labels: '' | ||||||
|  | assignees: '' | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								.github/branch-rename.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.github/branch-rename.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | modernize your local checkout of the repo like so, | ||||||
|  | ```sh | ||||||
|  | git branch -m master hovudstraum | ||||||
|  | git fetch origin | ||||||
|  | git branch -u origin/hovudstraum hovudstraum | ||||||
|  | git remote set-head origin -a | ||||||
|  | ``` | ||||||
							
								
								
									
										2
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | To show that your contribution is compatible with the MIT License, please include the following text somewhere in this PR description:   | ||||||
|  | This PR complies with the DCO; https://developercertificate.org/   | ||||||
							
								
								
									
										29
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,18 +5,39 @@ __pycache__/ | |||||||
| MANIFEST.in | MANIFEST.in | ||||||
| MANIFEST | MANIFEST | ||||||
| copyparty.egg-info/ | copyparty.egg-info/ | ||||||
| buildenv/ |  | ||||||
| build/ |  | ||||||
| dist/ |  | ||||||
| sfx/ |  | ||||||
| .venv/ | .venv/ | ||||||
|  |  | ||||||
|  | /buildenv/ | ||||||
|  | /build/ | ||||||
|  | /dist/ | ||||||
|  | /py2/ | ||||||
|  | /sfx* | ||||||
|  | /unt/ | ||||||
|  | /log/ | ||||||
|  |  | ||||||
| # ide | # ide | ||||||
| *.sublime-workspace | *.sublime-workspace | ||||||
|  |  | ||||||
| # winmerge | # winmerge | ||||||
| *.bak | *.bak | ||||||
|  |  | ||||||
|  | # apple pls | ||||||
|  | .DS_Store | ||||||
|  |  | ||||||
| # derived | # derived | ||||||
|  | copyparty/res/COPYING.txt | ||||||
| copyparty/web/deps/ | copyparty/web/deps/ | ||||||
| srv/ | srv/ | ||||||
|  | scripts/docker/i/ | ||||||
|  | contrib/package/arch/pkg/ | ||||||
|  | contrib/package/arch/src/ | ||||||
|  |  | ||||||
|  | # state/logs | ||||||
|  | up.*.txt | ||||||
|  | .hist/ | ||||||
|  | scripts/docker/*.out | ||||||
|  | scripts/docker/*.err | ||||||
|  | /perf.* | ||||||
|  |  | ||||||
|  | # nix build output link | ||||||
|  | result | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,7 @@ | |||||||
|             "module": "copyparty", |             "module": "copyparty", | ||||||
|             "console": "integratedTerminal", |             "console": "integratedTerminal", | ||||||
|             "cwd": "${workspaceFolder}", |             "cwd": "${workspaceFolder}", | ||||||
|  |             "justMyCode": false, | ||||||
|             "args": [ |             "args": [ | ||||||
|                 //"-nw", |                 //"-nw", | ||||||
|                 "-ed", |                 "-ed", | ||||||
| @@ -17,7 +18,7 @@ | |||||||
|                 "-mtp", |                 "-mtp", | ||||||
|                 ".bpm=f,bin/mtag/audio-bpm.py", |                 ".bpm=f,bin/mtag/audio-bpm.py", | ||||||
|                 "-aed:wark", |                 "-aed:wark", | ||||||
|                 "-vsrv::r:aed:cnodupe", |                 "-vsrv::r:rw,ed:c,dupe", | ||||||
|                 "-vdist:dist:r" |                 "-vdist:dist:r" | ||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										18
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,3 +1,5 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
| # takes arguments from launch.json | # takes arguments from launch.json | ||||||
| # is used by no_dbg in tasks.json | # is used by no_dbg in tasks.json | ||||||
| # launches 10x faster than mspython debugpy | # launches 10x faster than mspython debugpy | ||||||
| @@ -9,15 +11,15 @@ import sys | |||||||
|  |  | ||||||
| print(sys.executable) | print(sys.executable) | ||||||
|  |  | ||||||
|  | import json5 | ||||||
| import shlex | import shlex | ||||||
| import jstyleson |  | ||||||
| import subprocess as sp | import subprocess as sp | ||||||
|  |  | ||||||
|  |  | ||||||
| with open(".vscode/launch.json", "r", encoding="utf-8") as f: | with open(".vscode/launch.json", "r", encoding="utf-8") as f: | ||||||
|     tj = f.read() |     tj = f.read() | ||||||
|  |  | ||||||
| oj = jstyleson.loads(tj) | oj = json5.loads(tj) | ||||||
| argv = oj["configurations"][0]["args"] | argv = oj["configurations"][0]["args"] | ||||||
|  |  | ||||||
| try: | try: | ||||||
| @@ -28,7 +30,17 @@ except: | |||||||
|  |  | ||||||
| argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv] | argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv] | ||||||
|  |  | ||||||
| if re.search(" -j ?[0-9]", " ".join(argv)): | sfx = "" | ||||||
|  | if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): | ||||||
|  |     sfx = sys.argv[1] | ||||||
|  |     sys.argv = [sys.argv[0]] + sys.argv[2:] | ||||||
|  |  | ||||||
|  | argv += sys.argv[1:] | ||||||
|  |  | ||||||
|  | if sfx: | ||||||
|  |     argv = [sys.executable, sfx] + argv | ||||||
|  |     sp.check_call(argv) | ||||||
|  | elif re.search(" -j ?[0-9]", " ".join(argv)): | ||||||
|     argv = [sys.executable, "-m", "copyparty"] + argv |     argv = [sys.executable, "-m", "copyparty"] + argv | ||||||
|     sp.check_call(argv) |     sp.check_call(argv) | ||||||
| else: | else: | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,6 @@ | |||||||
|         "terminal.ansiBrightWhite": "#ffffff", |         "terminal.ansiBrightWhite": "#ffffff", | ||||||
|     }, |     }, | ||||||
|     "python.testing.pytestEnabled": false, |     "python.testing.pytestEnabled": false, | ||||||
|     "python.testing.nosetestsEnabled": false, |  | ||||||
|     "python.testing.unittestEnabled": true, |     "python.testing.unittestEnabled": true, | ||||||
|     "python.testing.unittestArgs": [ |     "python.testing.unittestArgs": [ | ||||||
|         "-v", |         "-v", | ||||||
| @@ -35,24 +34,30 @@ | |||||||
|     "python.linting.pylintEnabled": true, |     "python.linting.pylintEnabled": true, | ||||||
|     "python.linting.flake8Enabled": true, |     "python.linting.flake8Enabled": true, | ||||||
|     "python.linting.banditEnabled": true, |     "python.linting.banditEnabled": true, | ||||||
|  |     "python.linting.mypyEnabled": true, | ||||||
|     "python.linting.flake8Args": [ |     "python.linting.flake8Args": [ | ||||||
|         "--max-line-length=120", |         "--max-line-length=120", | ||||||
|         "--ignore=E722,F405,E203,W503,W293,E402", |         "--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226", | ||||||
|     ], |     ], | ||||||
|     "python.linting.banditArgs": [ |     "python.linting.banditArgs": [ | ||||||
|         "--ignore=B104" |         "--ignore=B104,B110,B112" | ||||||
|     ], |     ], | ||||||
|     "python.formatting.provider": "black", |     // python3 -m isort --py=27 --profile=black copyparty/ | ||||||
|  |     "python.formatting.provider": "none", | ||||||
|  |     "[python]": { | ||||||
|  |         "editor.defaultFormatter": "ms-python.black-formatter" | ||||||
|  |     }, | ||||||
|     "editor.formatOnSave": true, |     "editor.formatOnSave": true, | ||||||
|     "[html]": { |     "[html]": { | ||||||
|         "editor.formatOnSave": false, |         "editor.formatOnSave": false, | ||||||
|  |         "editor.autoIndent": "keep", | ||||||
|  |     }, | ||||||
|  |     "[css]": { | ||||||
|  |         "editor.formatOnSave": false, | ||||||
|     }, |     }, | ||||||
|     "files.associations": { |     "files.associations": { | ||||||
|         "*.makefile": "makefile" |         "*.makefile": "makefile" | ||||||
|     }, |     }, | ||||||
|     "python.formatting.blackArgs": [ |  | ||||||
|         "-t", |  | ||||||
|         "py27" |  | ||||||
|     ], |  | ||||||
|     "python.linting.enabled": true, |     "python.linting.enabled": true, | ||||||
|  |     "python.pythonPath": "/usr/bin/python3" | ||||||
| } | } | ||||||
							
								
								
									
										24
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | in the words of Abraham Lincoln: | ||||||
|  |  | ||||||
|  | > Be excellent to each other... and... PARTY ON, DUDES! | ||||||
|  |  | ||||||
|  | more specifically I'll paraphrase some examples from a german automotive corporation as they cover all the bases without being too wordy | ||||||
|  |  | ||||||
|  | ## Examples of unacceptable behavior | ||||||
|  | * intimidation, harassment, trolling | ||||||
|  | * insulting, derogatory, harmful or prejudicial comments | ||||||
|  | * posting private information without permission | ||||||
|  | * political or personal attacks | ||||||
|  |  | ||||||
|  | ## Examples of expected behavior | ||||||
|  | * being nice, friendly, welcoming, inclusive, mindful and empathetic | ||||||
|  | * acting considerate, modest, respectful | ||||||
|  | * using polite and inclusive language | ||||||
|  | * criticize constructively and accept constructive criticism | ||||||
|  | * respect different points of view | ||||||
|  |  | ||||||
|  | ## finally and even more specifically, | ||||||
|  | * parse opinions and feedback objectively without prejudice | ||||||
|  |   * it's the message that matters, not who said it | ||||||
|  |  | ||||||
|  | aaand that's how you say `be nice` in a way that fills half a floppy w | ||||||
							
								
								
									
										3
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | * do something cool | ||||||
|  |  | ||||||
|  | really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight | ||||||
							
								
								
									
										9
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | # Security Policy | ||||||
|  |  | ||||||
|  | if you hit something extra juicy pls let me know on either of the following | ||||||
|  | * email -- `copyparty@ocv.ze` except `ze` should be `me` | ||||||
|  | * [mastodon dm](https://layer8.space/@tripflag) -- `@tripflag@layer8.space` | ||||||
|  | * [github private vulnerability report](https://github.com/9001/copyparty/security/advisories/new), wow that form is complicated | ||||||
|  | * [twitter dm](https://twitter.com/tripflag) (if im somehow not banned yet) | ||||||
|  |  | ||||||
|  | no bug bounties sorry! all i can offer is greetz in the release notes | ||||||
| @@ -1,4 +1,18 @@ | |||||||
| # [`copyparty-fuse.py`](copyparty-fuse.py) | # [`u2c.py`](u2c.py) | ||||||
|  | * command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm) | ||||||
|  | * file uploads, file-search, autoresume of aborted/broken uploads | ||||||
|  | * sync local folder to server | ||||||
|  | * generally faster than browsers | ||||||
|  | * if something breaks just restart it | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # [`partyjournal.py`](partyjournal.py) | ||||||
|  | produces a chronological list of all uploads by collecting info from up2k databases and the filesystem | ||||||
|  | * outputs a standalone html file | ||||||
|  | * optional mapping from IP-addresses to nicknames | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # [`partyfuse.py`](partyfuse.py) | ||||||
| * mount a copyparty server as a local filesystem (read-only) | * mount a copyparty server as a local filesystem (read-only) | ||||||
| * **supports Windows!** -- expect `194 MiB/s` sequential read | * **supports Windows!** -- expect `194 MiB/s` sequential read | ||||||
| * **supports Linux** -- expect `117 MiB/s` sequential read | * **supports Linux** -- expect `117 MiB/s` sequential read | ||||||
| @@ -17,19 +31,19 @@ also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x perfor | |||||||
| * install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/) | * install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/) | ||||||
|   * [x] add python 3.x to PATH (it asks during install) |   * [x] add python 3.x to PATH (it asks during install) | ||||||
| * `python -m pip install --user fusepy` | * `python -m pip install --user fusepy` | ||||||
| * `python ./copyparty-fuse.py n: http://192.168.1.69:3923/` | * `python ./partyfuse.py n: http://192.168.1.69:3923/` | ||||||
|  |  | ||||||
| 10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled: | 10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled: | ||||||
| * `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}` | * `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}` | ||||||
| * `/mingw64/bin/python3 -m pip install --user fusepy` | * `/mingw64/bin/python3 -m pip install --user fusepy` | ||||||
| * `/mingw64/bin/python3 ./copyparty-fuse.py [...]` | * `/mingw64/bin/python3 ./partyfuse.py [...]` | ||||||
|  |  | ||||||
| you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)   | you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)   | ||||||
| (winfsp's sshfs leaks, doesn't look like winfsp itself does, should be fine) | (winfsp's sshfs leaks, doesn't look like winfsp itself does, should be fine) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # [`copyparty-fuse🅱️.py`](copyparty-fuseb.py) | # [`partyfuse2.py`](partyfuse2.py) | ||||||
| * mount a copyparty server as a local filesystem (read-only) | * mount a copyparty server as a local filesystem (read-only) | ||||||
| * does the same thing except more correct, `samba` approves | * does the same thing except more correct, `samba` approves | ||||||
| * **supports Linux** -- expect `18 MiB/s` (wait what) | * **supports Linux** -- expect `18 MiB/s` (wait what) | ||||||
| @@ -37,7 +51,7 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # [`copyparty-fuse-streaming.py`](copyparty-fuse-streaming.py) | # [`partyfuse-streaming.py`](partyfuse-streaming.py) | ||||||
| * pretend this doesn't exist | * pretend this doesn't exist | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -47,6 +61,7 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas | |||||||
| * copyparty can Popen programs like these during file indexing to collect additional metadata | * copyparty can Popen programs like these during file indexing to collect additional metadata | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # [`dbtool.py`](dbtool.py) | # [`dbtool.py`](dbtool.py) | ||||||
| upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db | upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db | ||||||
|  |  | ||||||
| @@ -61,3 +76,9 @@ cd /mnt/nas/music/.hist | |||||||
| ~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key | ~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key | ||||||
| ~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac | ~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # [`prisonparty.sh`](prisonparty.sh) | ||||||
|  | * run copyparty in a chroot, preventing any accidental file access | ||||||
|  | * creates bindmounts for /bin, /lib, and so on, see `sysdirs=` | ||||||
|   | |||||||
							
								
								
									
										111
									
								
								bin/dbtool.py
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								bin/dbtool.py
									
									
									
									
									
								
							| @@ -8,7 +8,10 @@ import sqlite3 | |||||||
| import argparse | import argparse | ||||||
|  |  | ||||||
| DB_VER1 = 3 | DB_VER1 = 3 | ||||||
| DB_VER2 = 4 | DB_VER2 = 5 | ||||||
|  |  | ||||||
|  | BY_PATH = None | ||||||
|  | NC = None | ||||||
|  |  | ||||||
|  |  | ||||||
| def die(msg): | def die(msg): | ||||||
| @@ -57,8 +60,13 @@ def compare(n1, d1, n2, d2, verbose): | |||||||
|         if rd.split("/", 1)[0] == ".hist": |         if rd.split("/", 1)[0] == ".hist": | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|         q = "select w from up where rd = ? and fn = ?" |         if BY_PATH: | ||||||
|         hit = d2.execute(q, (rd, fn)).fetchone() |             q = "select w from up where rd = ? and fn = ?" | ||||||
|  |             hit = d2.execute(q, (rd, fn)).fetchone() | ||||||
|  |         else: | ||||||
|  |             q = "select w from up where substr(w,1,16) = ? and +w = ?" | ||||||
|  |             hit = d2.execute(q, (w1[:16], w1)).fetchone() | ||||||
|  |  | ||||||
|         if not hit: |         if not hit: | ||||||
|             miss += 1 |             miss += 1 | ||||||
|             if verbose: |             if verbose: | ||||||
| @@ -70,27 +78,32 @@ def compare(n1, d1, n2, d2, verbose): | |||||||
|     n = 0 |     n = 0 | ||||||
|     miss = {} |     miss = {} | ||||||
|     nmiss = 0 |     nmiss = 0 | ||||||
|     for w1, k, v in d1.execute("select * from mt"): |     for w1s, k, v in d1.execute("select * from mt"): | ||||||
|  |  | ||||||
|         n += 1 |         n += 1 | ||||||
|         if n % 100_000 == 0: |         if n % 100_000 == 0: | ||||||
|             m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m" |             m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m" | ||||||
|             print(m) |             print(m) | ||||||
|  |  | ||||||
|         q = "select rd, fn from up where substr(w,1,16) = ?" |         q = "select w, rd, fn from up where substr(w,1,16) = ?" | ||||||
|         rd, fn = d1.execute(q, (w1,)).fetchone() |         w1, rd, fn = d1.execute(q, (w1s,)).fetchone() | ||||||
|         if rd.split("/", 1)[0] == ".hist": |         if rd.split("/", 1)[0] == ".hist": | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|         q = "select substr(w,1,16) from up where rd = ? and fn = ?" |         if BY_PATH: | ||||||
|         w2 = d2.execute(q, (rd, fn)).fetchone() |             q = "select w from up where rd = ? and fn = ?" | ||||||
|  |             w2 = d2.execute(q, (rd, fn)).fetchone() | ||||||
|  |         else: | ||||||
|  |             q = "select w from up where substr(w,1,16) = ? and +w = ?" | ||||||
|  |             w2 = d2.execute(q, (w1s, w1)).fetchone() | ||||||
|  |  | ||||||
|         if w2: |         if w2: | ||||||
|             w2 = w2[0] |             w2 = w2[0] | ||||||
|  |  | ||||||
|         v2 = None |         v2 = None | ||||||
|         if w2: |         if w2: | ||||||
|             v2 = d2.execute( |             v2 = d2.execute( | ||||||
|                 "select v from mt where w = ? and +k = ?", (w2, k) |                 "select v from mt where w = ? and +k = ?", (w2[:16], k) | ||||||
|             ).fetchone() |             ).fetchone() | ||||||
|             if v2: |             if v2: | ||||||
|                 v2 = v2[0] |                 v2 = v2[0] | ||||||
| @@ -124,7 +137,7 @@ def compare(n1, d1, n2, d2, verbose): | |||||||
|  |  | ||||||
|     for k, v in sorted(miss.items()): |     for k, v in sorted(miss.items()): | ||||||
|         if v: |         if v: | ||||||
|             print(f"{n1} has {v:6} more {k:<6} tags than {n2}") |             print(f"{n1} has {v:7} more {k:<7} tags than {n2}") | ||||||
|  |  | ||||||
|     print(f"in total, {nmiss} missing tags in {n2}\n") |     print(f"in total, {nmiss} missing tags in {n2}\n") | ||||||
|  |  | ||||||
| @@ -132,47 +145,75 @@ def compare(n1, d1, n2, d2, verbose): | |||||||
| def copy_mtp(d1, d2, tag, rm): | def copy_mtp(d1, d2, tag, rm): | ||||||
|     nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0] |     nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0] | ||||||
|     n = 0 |     n = 0 | ||||||
|     ndone = 0 |     ncopy = 0 | ||||||
|     for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)): |     nskip = 0 | ||||||
|  |     for w1s, k, v in d1.execute("select * from mt where k = ?", (tag,)): | ||||||
|         n += 1 |         n += 1 | ||||||
|         if n % 25_000 == 0: |         if n % 25_000 == 0: | ||||||
|             m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m" |             m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ncopy} copied, {nskip} skipped\033[0m" | ||||||
|             print(m) |             print(m) | ||||||
|  |  | ||||||
|         q = "select rd, fn from up where substr(w,1,16) = ?" |         q = "select w, rd, fn from up where substr(w,1,16) = ?" | ||||||
|         rd, fn = d1.execute(q, (w1,)).fetchone() |         w1, rd, fn = d1.execute(q, (w1s,)).fetchone() | ||||||
|         if rd.split("/", 1)[0] == ".hist": |         if rd.split("/", 1)[0] == ".hist": | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|         q = "select substr(w,1,16) from up where rd = ? and fn = ?" |         if BY_PATH: | ||||||
|         w2 = d2.execute(q, (rd, fn)).fetchone() |             q = "select w from up where rd = ? and fn = ?" | ||||||
|  |             w2 = d2.execute(q, (rd, fn)).fetchone() | ||||||
|  |         else: | ||||||
|  |             q = "select w from up where substr(w,1,16) = ? and +w = ?" | ||||||
|  |             w2 = d2.execute(q, (w1s, w1)).fetchone() | ||||||
|  |  | ||||||
|         if not w2: |         if not w2: | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|         w2 = w2[0] |         w2s = w2[0][:16] | ||||||
|         hit = d2.execute("select v from mt where w = ? and +k = ?", (w2, k)).fetchone() |         hit = d2.execute("select v from mt where w = ? and +k = ?", (w2s, k)).fetchone() | ||||||
|         if hit: |         if hit: | ||||||
|             hit = hit[0] |             hit = hit[0] | ||||||
|  |  | ||||||
|         if hit != v: |         if hit != v: | ||||||
|             ndone += 1 |             if NC and hit is not None: | ||||||
|             if hit is not None: |                 nskip += 1 | ||||||
|                 d2.execute("delete from mt where w = ? and +k = ?", (w2, k)) |                 continue | ||||||
|  |  | ||||||
|             d2.execute("insert into mt values (?,?,?)", (w2, k, v)) |             ncopy += 1 | ||||||
|  |             if hit is not None: | ||||||
|  |                 d2.execute("delete from mt where w = ? and +k = ?", (w2s, k)) | ||||||
|  |  | ||||||
|  |             d2.execute("insert into mt values (?,?,?)", (w2s, k, v)) | ||||||
|             if rm: |             if rm: | ||||||
|                 d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,)) |                 d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2s,)) | ||||||
|  |  | ||||||
|     d2.commit() |     d2.commit() | ||||||
|     print(f"copied {ndone} {tag} tags over") |     print(f"copied {ncopy} {tag} tags over, skipped {nskip}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def examples(): | ||||||
|  |     print( | ||||||
|  |         """ | ||||||
|  | # clearing the journal | ||||||
|  | ./dbtool.py up2k.db | ||||||
|  |  | ||||||
|  | # copy tags ".bpm" and "key" from old.db to up2k.db, and remove the mtp flag from matching files (so copyparty won't run any mtps on it) | ||||||
|  | ./dbtool.py -ls up2k.db | ||||||
|  | ./dbtool.py -src old.db up2k.db -cmp | ||||||
|  | ./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy key | ||||||
|  | ./dbtool.py -src old.v3 up2k.db -rm-mtp-flag -copy .bpm -vac | ||||||
|  |  | ||||||
|  | """ | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|  |     global NC, BY_PATH | ||||||
|     os.system("") |     os.system("") | ||||||
|     print() |     print() | ||||||
|  |  | ||||||
|     ap = argparse.ArgumentParser() |     ap = argparse.ArgumentParser() | ||||||
|     ap.add_argument("db", help="database to work on") |     ap.add_argument("db", help="database to work on") | ||||||
|  |     ap.add_argument("-h2", action="store_true", help="show examples") | ||||||
|     ap.add_argument("-src", metavar="DB", type=str, help="database to copy from") |     ap.add_argument("-src", metavar="DB", type=str, help="database to copy from") | ||||||
|  |  | ||||||
|     ap2 = ap.add_argument_group("informational / read-only stuff") |     ap2 = ap.add_argument_group("informational / read-only stuff") | ||||||
| @@ -185,11 +226,29 @@ def main(): | |||||||
|     ap2.add_argument( |     ap2.add_argument( | ||||||
|         "-rm-mtp-flag", |         "-rm-mtp-flag", | ||||||
|         action="store_true", |         action="store_true", | ||||||
|         help="when an mtp tag is copied over, also mark that as done, so copyparty won't run mtp on it", |         help="when an mtp tag is copied over, also mark that file as done, so copyparty won't run any mtps on those files", | ||||||
|     ) |     ) | ||||||
|     ap2.add_argument("-vac", action="store_true", help="optimize DB") |     ap2.add_argument("-vac", action="store_true", help="optimize DB") | ||||||
|  |  | ||||||
|  |     ap2 = ap.add_argument_group("behavior modifiers") | ||||||
|  |     ap2.add_argument( | ||||||
|  |         "-nc", | ||||||
|  |         action="store_true", | ||||||
|  |         help="no-clobber; don't replace/overwrite existing tags", | ||||||
|  |     ) | ||||||
|  |     ap2.add_argument( | ||||||
|  |         "-by-path", | ||||||
|  |         action="store_true", | ||||||
|  |         help="match files based on location rather than warks (content-hash), use this if the databases have different wark salts", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     ar = ap.parse_args() |     ar = ap.parse_args() | ||||||
|  |     if ar.h2: | ||||||
|  |         examples() | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     NC = ar.nc | ||||||
|  |     BY_PATH = ar.by_path | ||||||
|  |  | ||||||
|     for v in [ar.db, ar.src]: |     for v in [ar.db, ar.src]: | ||||||
|         if v and not os.path.exists(v): |         if v and not os.path.exists(v): | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								bin/handlers/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								bin/handlers/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | replace the standard 404 / 403 responses with plugins | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # usage | ||||||
|  |  | ||||||
|  | load plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # api | ||||||
|  |  | ||||||
|  | each plugin must define a `main()` which takes 3 arguments; | ||||||
|  |  | ||||||
|  | * `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself) | ||||||
|  | * `vn` is the VFS which overlaps with the requested URL, and | ||||||
|  | * `rem` is the URL remainder below the VFS mountpoint | ||||||
|  |     * so `vn.vpath + rem` == `cli.vpath` == original request | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # examples | ||||||
|  |  | ||||||
|  | ## on404 | ||||||
|  |  | ||||||
|  | * [sorry.py](answer.py) replies with a custom message instead of the usual 404 | ||||||
|  | * [nooo.py](nooo.py) replies with an endless noooooooooooooo | ||||||
|  | * [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary | ||||||
|  | * [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff | ||||||
|  |  | ||||||
|  | ## on403 | ||||||
|  |  | ||||||
|  | * [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # notes | ||||||
|  |  | ||||||
|  | * on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404) | ||||||
							
								
								
									
										36
									
								
								bin/handlers/caching-proxy.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										36
									
								
								bin/handlers/caching-proxy.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | # assume each requested file exists on another webserver and | ||||||
|  | # download + mirror them as they're requested | ||||||
|  | # (basically pretend we're warnish) | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import requests | ||||||
|  |  | ||||||
|  | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from copyparty.httpcli import HttpCli | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(cli: "HttpCli", vn, rem): | ||||||
|  |     url = "https://mirrors.edge.kernel.org/alpine/" + rem | ||||||
|  |     abspath = os.path.join(vn.realpath, rem) | ||||||
|  |  | ||||||
|  |     # sneaky trick to preserve a requests-session between downloads | ||||||
|  |     # so it doesn't have to spend ages reopening https connections; | ||||||
|  |     # luckily we can stash it inside the copyparty client session, | ||||||
|  |     # name just has to be definitely unused so "hacapo_req_s" it is | ||||||
|  |     req_s = getattr(cli.conn, "hacapo_req_s", None) or requests.Session() | ||||||
|  |     setattr(cli.conn, "hacapo_req_s", req_s) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         os.makedirs(os.path.dirname(abspath), exist_ok=True) | ||||||
|  |         with req_s.get(url, stream=True, timeout=69) as r: | ||||||
|  |             r.raise_for_status() | ||||||
|  |             with open(abspath, "wb", 64 * 1024) as f: | ||||||
|  |                 for buf in r.iter_content(chunk_size=64 * 1024): | ||||||
|  |                     f.write(buf) | ||||||
|  |     except: | ||||||
|  |         os.unlink(abspath) | ||||||
|  |         return "false" | ||||||
|  |  | ||||||
|  |     return "retry" | ||||||
							
								
								
									
										6
									
								
								bin/handlers/ip-ok.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								bin/handlers/ip-ok.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | # disable permission checks and allow access if client-ip is 1.2.3.4 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(cli, vn, rem): | ||||||
|  |     if cli.ip == "1.2.3.4": | ||||||
|  |         return "allow" | ||||||
							
								
								
									
										11
									
								
								bin/handlers/never404.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								bin/handlers/never404.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | # create a dummy file and let copyparty return it | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(cli, vn, rem): | ||||||
|  |     print("hello", cli.ip) | ||||||
|  |  | ||||||
|  |     abspath = vn.canonical(rem) | ||||||
|  |     with open(abspath, "wb") as f: | ||||||
|  |         f.write(b"404? not on MY watch!") | ||||||
|  |  | ||||||
|  |     return "retry" | ||||||
							
								
								
									
										16
									
								
								bin/handlers/nooo.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										16
									
								
								bin/handlers/nooo.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | # reply with an endless "noooooooooooooooooooooooo" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def say_no(): | ||||||
|  |     yield b"n" | ||||||
|  |     while True: | ||||||
|  |         yield b"o" * 4096 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(cli, vn, rem): | ||||||
|  |     cli.send_headers(None, 404, "text/plain") | ||||||
|  |  | ||||||
|  |     for chunk in say_no(): | ||||||
|  |         cli.s.sendall(chunk) | ||||||
|  |  | ||||||
|  |     return "false" | ||||||
							
								
								
									
										7
									
								
								bin/handlers/sorry.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								bin/handlers/sorry.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | # sends a custom response instead of the usual 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(cli, vn, rem): | ||||||
|  |     msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist" | ||||||
|  |  | ||||||
|  |     return str(cli.reply(msg.encode("utf-8"), 404, "text/plain")) | ||||||
							
								
								
									
										29
									
								
								bin/hooks/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								bin/hooks/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | standalone programs which are executed by copyparty when an event happens (upload, file rename, delete, ...) | ||||||
|  |  | ||||||
|  | these programs either take zero arguments, or a filepath (the affected file), or a json message with filepath + additional info | ||||||
|  |  | ||||||
|  | run copyparty with `--help-hooks` for usage details / hook type explanations (xbu/xau/xiu/xbr/xar/xbd/xad) | ||||||
|  |  | ||||||
|  | > **note:** in addition to event hooks (the stuff described here), copyparty has another api to run your programs/scripts while providing way more information such as audio tags / video codecs / etc and optionally daisychaining data between scripts in a processing pipeline; if that's what you want then see [mtp plugins](../mtag/) instead | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # after upload | ||||||
|  | * [notify.py](notify.py) shows a desktop notification ([example](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png)) | ||||||
|  |   * [notify2.py](notify2.py) uses the json API to show more context | ||||||
|  | * [image-noexif.py](image-noexif.py) removes image exif by overwriting / directly editing the uploaded file | ||||||
|  | * [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png)) | ||||||
|  | * [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # upload batches | ||||||
|  | these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every single file), `xiu` hooks are given a list of recent uploads on STDIN after the server has gone idle for N seconds, reducing server load + providing more context | ||||||
|  | * [xiu.py](xiu.py) is a "minimal" example showing a list of filenames + total filesize | ||||||
|  | * [xiu-sha.py](xiu-sha.py) produces a sha512 checksum list in the volume root | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # before upload | ||||||
|  | * [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # on message | ||||||
|  | * [wget.py](wget.py) lets you download files by POSTing URLs to copyparty | ||||||
							
								
								
									
										68
									
								
								bin/hooks/discord-announce.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								bin/hooks/discord-announce.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import requests | ||||||
|  | from copyparty.util import humansize, quotep | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | announces a new upload on discord | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xau f,t5,j,bin/hooks/discord-announce.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xau=f,t5,j,bin/hooks/discord-announce.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xbu = execute after upload | ||||||
|  |     f  = fork; don't wait for it to finish | ||||||
|  |     t5 = timeout if it's still running after 5 sec | ||||||
|  |     j  = provide upload information as json; not just the filename | ||||||
|  |  | ||||||
|  | replace "xau" with "xbu" to announce Before upload starts instead of After completion | ||||||
|  |  | ||||||
|  | # how to discord: | ||||||
|  | first create the webhook url; https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks | ||||||
|  | then use this to design your message: https://discohook.org/ | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     WEBHOOK = "https://discord.com/api/webhooks/1234/base64" | ||||||
|  |     WEBHOOK = "https://discord.com/api/webhooks/1066830390280597718/M1TDD110hQA-meRLMRhdurych8iyG35LDoI1YhzbrjGP--BXNZodZFczNVwK4Ce7Yme5" | ||||||
|  |  | ||||||
|  |     # read info from copyparty | ||||||
|  |     inf = json.loads(sys.argv[1]) | ||||||
|  |     vpath = inf["vp"] | ||||||
|  |     filename = vpath.split("/")[-1] | ||||||
|  |     url = f"https://{inf['host']}/{quotep(vpath)}" | ||||||
|  |  | ||||||
|  |     # compose the message to discord | ||||||
|  |     j = { | ||||||
|  |         "title": filename, | ||||||
|  |         "url": url, | ||||||
|  |         "description": url.rsplit("/", 1)[0], | ||||||
|  |         "color": 0x449900, | ||||||
|  |         "fields": [ | ||||||
|  |             {"name": "Size", "value": humansize(inf["sz"])}, | ||||||
|  |             {"name": "User", "value": inf["user"]}, | ||||||
|  |             {"name": "IP", "value": inf["ip"]}, | ||||||
|  |         ], | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for v in j["fields"]: | ||||||
|  |         v["inline"] = True | ||||||
|  |  | ||||||
|  |     r = requests.post(WEBHOOK, json={"embeds": [j]}) | ||||||
|  |     print(f"discord: {r}\n", end="") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										72
									
								
								bin/hooks/image-noexif.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										72
									
								
								bin/hooks/image-noexif.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | remove exif tags from uploaded images; the eventhook edition of | ||||||
|  | https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/image-noexif.py | ||||||
|  |  | ||||||
|  | dependencies: | ||||||
|  |     exiftool / perl-Image-ExifTool | ||||||
|  |  | ||||||
|  | being an upload hook, this will take effect after upload completion | ||||||
|  |     but before copyparty has hashed/indexed the file, which means that | ||||||
|  |     copyparty will never index the original file, so deduplication will | ||||||
|  |     not work as expected... which is mostly OK but ehhh | ||||||
|  |  | ||||||
|  | note: modifies the file in-place, so don't set the `f` (fork) flag | ||||||
|  |  | ||||||
|  | example usages; either as global config (all volumes) or as volflag: | ||||||
|  |     --xau bin/hooks/image-noexif.py | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xau=bin/hooks/image-noexif.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |     share fs-path srv/inc at /inc (readable by all, read-write for user ed) | ||||||
|  |     running this xau (execute-after-upload) plugin for all uploaded files | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # filetypes to process; ignores everything else | ||||||
|  | EXTS = ("jpg", "jpeg", "avif", "heif", "heic") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p.encode("utf-8") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = sys.argv[1] | ||||||
|  |     ext = fp.lower().split(".")[-1] | ||||||
|  |     if ext not in EXTS: | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     cwd, fn = os.path.split(fp) | ||||||
|  |     os.chdir(cwd) | ||||||
|  |     f1 = fsenc(fn) | ||||||
|  |     cmd = [ | ||||||
|  |         b"exiftool", | ||||||
|  |         b"-exif:all=", | ||||||
|  |         b"-iptc:all=", | ||||||
|  |         b"-xmp:all=", | ||||||
|  |         b"-P", | ||||||
|  |         b"-overwrite_original", | ||||||
|  |         b"--", | ||||||
|  |         f1, | ||||||
|  |     ] | ||||||
|  |     sp.check_output(cmd) | ||||||
|  |     print("image-noexif: stripped") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     try: | ||||||
|  |         main() | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
							
								
								
									
										115
									
								
								bin/hooks/msg-log.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										115
									
								
								bin/hooks/msg-log.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # coding: utf-8 | ||||||
|  | # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab | ||||||
|  | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  | from datetime import datetime | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | use copyparty as a dumb messaging server / guestbook thing; | ||||||
|  | initially contributed by @clach04 in https://github.com/9001/copyparty/issues/35 (thanks!) | ||||||
|  |  | ||||||
|  | Sample usage: | ||||||
|  |  | ||||||
|  |     python copyparty-sfx.py --xm j,bin/hooks/msg-log.py | ||||||
|  |  | ||||||
|  | Where: | ||||||
|  |  | ||||||
|  |     xm = execute on message-to-server-log | ||||||
|  |     j = provide message information as json; not just the text - this script REQUIRES json | ||||||
|  |     t10 = timeout and kill download after 10 secs | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # output filename | ||||||
|  | FILENAME = os.environ.get("COPYPARTY_MESSAGE_FILENAME", "") or "README.md" | ||||||
|  |  | ||||||
|  | # set True to write in descending order (newest message at top of file); | ||||||
|  | # note that this becomes very slow/expensive as the file gets bigger | ||||||
|  | DESCENDING = True | ||||||
|  |  | ||||||
|  | # the message template; the following parameters are provided by copyparty and can be referenced below: | ||||||
|  | # 'ap' = absolute filesystem path where the message was posted | ||||||
|  | # 'vp' = virtual path (URL 'path') where the message was posted | ||||||
|  | # 'mt' = 'at' = unix-timestamp when the message was posted | ||||||
|  | # 'datetime' = ISO-8601 time when the message was posted | ||||||
|  | # 'sz' = message size in bytes | ||||||
|  | # 'host' = the server hostname which the user was accessing (URL 'host') | ||||||
|  | # 'user' = username (if logged in), otherwise '*' | ||||||
|  | # 'txt' = the message text itself | ||||||
|  | # (uncomment the print(msg_info) to see if additional information has been introduced by copyparty since this was written) | ||||||
|  | TEMPLATE = """ | ||||||
|  | 🕒 %(datetime)s, 👤 %(user)s @ %(ip)s | ||||||
|  | %(txt)s | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_ascending(filepath, msg_text): | ||||||
|  |     with open(filepath, "a", encoding="utf-8", errors="replace") as outfile: | ||||||
|  |         outfile.write(msg_text) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_descending(filepath, msg_text): | ||||||
|  |     lockpath = filepath + ".lock" | ||||||
|  |     got_it = False | ||||||
|  |     for _ in range(16): | ||||||
|  |         try: | ||||||
|  |             os.mkdir(lockpath) | ||||||
|  |             got_it = True | ||||||
|  |             break | ||||||
|  |         except: | ||||||
|  |             time.sleep(0.1) | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |     if not got_it: | ||||||
|  |         return sys.exit(1) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         oldpath = filepath + ".old" | ||||||
|  |         os.rename(filepath, oldpath) | ||||||
|  |         with open(oldpath, "r", encoding="utf-8", errors="replace") as infile, open( | ||||||
|  |             filepath, "w", encoding="utf-8", errors="replace" | ||||||
|  |         ) as outfile: | ||||||
|  |             outfile.write(msg_text) | ||||||
|  |             while True: | ||||||
|  |                 buf = infile.read(4096) | ||||||
|  |                 if not buf: | ||||||
|  |                     break | ||||||
|  |                 outfile.write(buf) | ||||||
|  |     finally: | ||||||
|  |         try: | ||||||
|  |             os.unlink(oldpath) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |         os.rmdir(lockpath) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(argv=None): | ||||||
|  |     if argv is None: | ||||||
|  |         argv = sys.argv | ||||||
|  |  | ||||||
|  |     msg_info = json.loads(sys.argv[1]) | ||||||
|  |     # print(msg_info) | ||||||
|  |  | ||||||
|  |     dt = datetime.utcfromtimestamp(msg_info["at"]) | ||||||
|  |     msg_info["datetime"] = dt.strftime("%Y-%m-%d, %H:%M:%S") | ||||||
|  |  | ||||||
|  |     msg_text = TEMPLATE % msg_info | ||||||
|  |  | ||||||
|  |     filepath = os.path.join(msg_info["ap"], FILENAME) | ||||||
|  |  | ||||||
|  |     if DESCENDING and os.path.exists(filepath): | ||||||
|  |         write_descending(filepath, msg_text) | ||||||
|  |     else: | ||||||
|  |         write_ascending(filepath, msg_text) | ||||||
|  |  | ||||||
|  |     print(msg_text) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										66
									
								
								bin/hooks/notify.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								bin/hooks/notify.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  | from plyer import notification | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | show os notification on upload; works on windows, linux, macos, android | ||||||
|  |  | ||||||
|  | depdencies: | ||||||
|  |     windows: python3 -m pip install --user -U plyer | ||||||
|  |     linux:   python3 -m pip install --user -U plyer | ||||||
|  |     macos:   python3 -m pip install --user -U plyer pyobjus | ||||||
|  |     android: just termux and termux-api | ||||||
|  |  | ||||||
|  | example usages; either as global config (all volumes) or as volflag: | ||||||
|  |     --xau f,bin/hooks/notify.py | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xau=f,bin/hooks/notify.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xau = execute after upload | ||||||
|  |     f   = fork so it doesn't block uploads | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import humansize | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def humansize(n): | ||||||
|  |         return n | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = sys.argv[1] | ||||||
|  |     dp, fn = os.path.split(fp) | ||||||
|  |     try: | ||||||
|  |         sz = humansize(os.path.getsize(fp)) | ||||||
|  |     except: | ||||||
|  |         sz = "?" | ||||||
|  |  | ||||||
|  |     msg = "{} ({})\n📁 {}".format(fn, sz, dp) | ||||||
|  |     title = "File received" | ||||||
|  |  | ||||||
|  |     if "com.termux" in sys.executable: | ||||||
|  |         sp.run(["termux-notification", "-t", title, "-c", msg]) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     icon = "emblem-documents-symbolic" if sys.platform == "linux" else "" | ||||||
|  |     notification.notify( | ||||||
|  |         title=title, | ||||||
|  |         message=msg, | ||||||
|  |         app_icon=icon, | ||||||
|  |         timeout=10, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										72
									
								
								bin/hooks/notify2.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										72
									
								
								bin/hooks/notify2.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  | from datetime import datetime | ||||||
|  | from plyer import notification | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | same as notify.py but with additional info (uploader, ...) | ||||||
|  | and also supports --xm (notify on 📟 message) | ||||||
|  |  | ||||||
|  | example usages; either as global config (all volumes) or as volflag: | ||||||
|  |     --xm  f,j,bin/hooks/notify2.py | ||||||
|  |     --xau f,j,bin/hooks/notify2.py | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xm=f,j,bin/hooks/notify2.py | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xau=f,j,bin/hooks/notify2.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all uploads / msgs with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xau = execute after upload | ||||||
|  |     f   = fork so it doesn't block uploads | ||||||
|  |     j   = provide json instead of filepath list | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import humansize | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def humansize(n): | ||||||
|  |         return n | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     inf = json.loads(sys.argv[1]) | ||||||
|  |     fp = inf["ap"] | ||||||
|  |     sz = humansize(inf["sz"]) | ||||||
|  |     dp, fn = os.path.split(fp) | ||||||
|  |     mt = datetime.utcfromtimestamp(inf["mt"]).strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |  | ||||||
|  |     msg = f"{fn} ({sz})\n📁 {dp}" | ||||||
|  |     title = "File received" | ||||||
|  |     icon = "emblem-documents-symbolic" if sys.platform == "linux" else "" | ||||||
|  |  | ||||||
|  |     if inf.get("txt"): | ||||||
|  |         msg = inf["txt"] | ||||||
|  |         title = "Message received" | ||||||
|  |         icon = "mail-unread-symbolic" if sys.platform == "linux" else "" | ||||||
|  |  | ||||||
|  |     msg += f"\n👤 {inf['user']} ({inf['ip']})\n🕒 {mt}" | ||||||
|  |  | ||||||
|  |     if "com.termux" in sys.executable: | ||||||
|  |         sp.run(["termux-notification", "-t", title, "-c", msg]) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     notification.notify( | ||||||
|  |         title=title, | ||||||
|  |         message=msg, | ||||||
|  |         app_icon=icon, | ||||||
|  |         timeout=10, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										35
									
								
								bin/hooks/reject-extension.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										35
									
								
								bin/hooks/reject-extension.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | reject file uploads by file extension | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xbu c,bin/hooks/reject-extension.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xbu=c,bin/hooks/reject-extension.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xbu = execute before upload | ||||||
|  |     c   = check result, reject upload if error | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     bad = "exe scr com pif bat ps1 jar msi" | ||||||
|  |  | ||||||
|  |     ext = sys.argv[1].split(".")[-1] | ||||||
|  |  | ||||||
|  |     sys.exit(1 if ext in bad.split() else 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										44
									
								
								bin/hooks/reject-mimetype.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										44
									
								
								bin/hooks/reject-mimetype.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  | import magic | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | reject file uploads by mimetype | ||||||
|  |  | ||||||
|  | dependencies (linux, macos): | ||||||
|  |     python3 -m pip install --user -U python-magic | ||||||
|  |  | ||||||
|  | dependencies (windows): | ||||||
|  |     python3 -m pip install --user -U python-magic-bin | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xau c,bin/hooks/reject-mimetype.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xau=c,bin/hooks/reject-mimetype.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xau = execute after upload | ||||||
|  |     c   = check result, reject upload if error | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     ok = ["image/jpeg", "image/png"] | ||||||
|  |  | ||||||
|  |     mt = magic.from_file(sys.argv[1], mime=True) | ||||||
|  |  | ||||||
|  |     print(mt) | ||||||
|  |  | ||||||
|  |     sys.exit(1 if mt not in ok else 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										64
									
								
								bin/hooks/wget.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										64
									
								
								bin/hooks/wget.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import subprocess as sp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | use copyparty as a file downloader by POSTing URLs as | ||||||
|  | application/x-www-form-urlencoded (for example using the | ||||||
|  | message/pager function on the website) | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xm f,j,t3600,bin/hooks/wget.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xm=f,j,t3600,bin/hooks/wget.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on all messages with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xm = execute on message-to-server-log | ||||||
|  |     f = fork so it doesn't block uploads | ||||||
|  |     j = provide message information as json; not just the text | ||||||
|  |     c3 = mute all output | ||||||
|  |     t3600 = timeout and kill download after 1 hour | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     inf = json.loads(sys.argv[1]) | ||||||
|  |     url = inf["txt"] | ||||||
|  |     if "://" not in url: | ||||||
|  |         url = "https://" + url | ||||||
|  |  | ||||||
|  |     proto = url.split("://")[0].lower() | ||||||
|  |     if proto not in ("http", "https", "ftp", "ftps"): | ||||||
|  |         raise Exception("bad proto {}".format(proto)) | ||||||
|  |  | ||||||
|  |     os.chdir(inf["ap"]) | ||||||
|  |  | ||||||
|  |     name = url.split("?")[0].split("/")[-1] | ||||||
|  |     tfn = "-- DOWNLOADING " + name | ||||||
|  |     print(f"{tfn}\n", end="") | ||||||
|  |     open(tfn, "wb").close() | ||||||
|  |  | ||||||
|  |     cmd = ["wget", "--trust-server-names", "-nv", "--", url] | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         sp.check_call(cmd) | ||||||
|  |     except: | ||||||
|  |         t = "-- FAILED TO DONWLOAD " + name | ||||||
|  |         print(f"{t}\n", end="") | ||||||
|  |         open(t, "wb").close() | ||||||
|  |  | ||||||
|  |     os.unlink(tfn) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										108
									
								
								bin/hooks/xiu-sha.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										108
									
								
								bin/hooks/xiu-sha.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import hashlib | ||||||
|  | import json | ||||||
|  | import sys | ||||||
|  | from datetime import datetime | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | this hook will produce a single sha512 file which | ||||||
|  | covers all recent uploads (plus metadata comments) | ||||||
|  |  | ||||||
|  | use this with --xiu, which makes copyparty buffer | ||||||
|  | uploads until server is idle, providing file infos | ||||||
|  | on stdin (filepaths or json) | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xiu i5,j,bin/hooks/xiu-sha.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xiu=i5,j,bin/hooks/xiu-sha.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on batches of uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xiu = execute after uploads... | ||||||
|  |     i5  = ...after volume has been idle for 5sec | ||||||
|  |     j   = provide json instead of filepath list | ||||||
|  |  | ||||||
|  | note the "f" (fork) flag is not set, so this xiu | ||||||
|  | will block other xiu hooks while it's running | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def humantime(ts): | ||||||
|  |     return datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def find_files_root(inf): | ||||||
|  |     di = 9000 | ||||||
|  |     for f1, f2 in zip(inf, inf[1:]): | ||||||
|  |         p1 = f1["ap"].replace("\\", "/").rsplit("/", 1)[0] | ||||||
|  |         p2 = f2["ap"].replace("\\", "/").rsplit("/", 1)[0] | ||||||
|  |         di = min(len(p1), len(p2), di) | ||||||
|  |         di = next((i for i in range(di) if p1[i] != p2[i]), di) | ||||||
|  |  | ||||||
|  |     return di + 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def find_vol_root(inf): | ||||||
|  |     return len(inf[0]["ap"][: -len(inf[0]["vp"])]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     zb = sys.stdin.buffer.read() | ||||||
|  |     zs = zb.decode("utf-8", "replace") | ||||||
|  |     inf = json.loads(zs) | ||||||
|  |  | ||||||
|  |     # root directory (where to put the sha512 file); | ||||||
|  |     # di = find_files_root(inf)  # next to the file closest to volume root | ||||||
|  |     di = find_vol_root(inf)  # top of the entire volume | ||||||
|  |  | ||||||
|  |     ret = [] | ||||||
|  |     total_sz = 0 | ||||||
|  |     for md in inf: | ||||||
|  |         ap = md["ap"] | ||||||
|  |         rp = ap[di:] | ||||||
|  |         total_sz += md["sz"] | ||||||
|  |         fsize = "{:,}".format(md["sz"]) | ||||||
|  |         mtime = humantime(md["mt"]) | ||||||
|  |         up_ts = humantime(md["at"]) | ||||||
|  |  | ||||||
|  |         h = hashlib.sha512() | ||||||
|  |         with open(fsenc(md["ap"]), "rb", 512 * 1024) as f: | ||||||
|  |             while True: | ||||||
|  |                 buf = f.read(512 * 1024) | ||||||
|  |                 if not buf: | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |                 h.update(buf) | ||||||
|  |  | ||||||
|  |         cksum = h.hexdigest() | ||||||
|  |         meta = " | ".join([md["wark"], up_ts, mtime, fsize, md["ip"]]) | ||||||
|  |         ret.append("# {}\n{} *{}".format(meta, cksum, rp)) | ||||||
|  |  | ||||||
|  |     ret.append("# {} files, {} bytes total".format(len(inf), total_sz)) | ||||||
|  |     ret.append("") | ||||||
|  |     ftime = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f") | ||||||
|  |     fp = "{}xfer-{}.sha512".format(inf[0]["ap"][:di], ftime) | ||||||
|  |     with open(fsenc(fp), "wb") as f: | ||||||
|  |         f.write("\n".join(ret).encode("utf-8", "replace")) | ||||||
|  |  | ||||||
|  |     print("wrote checksums to {}".format(fp)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										50
									
								
								bin/hooks/xiu.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										50
									
								
								bin/hooks/xiu.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | this hook prints absolute filepaths + total size | ||||||
|  |  | ||||||
|  | use this with --xiu, which makes copyparty buffer | ||||||
|  | uploads until server is idle, providing file infos | ||||||
|  | on stdin (filepaths or json) | ||||||
|  |  | ||||||
|  | example usage as global config: | ||||||
|  |     --xiu i1,j,bin/hooks/xiu.py | ||||||
|  |  | ||||||
|  | example usage as a volflag (per-volume config): | ||||||
|  |     -v srv/inc:inc:r:rw,ed:c,xiu=i1,j,bin/hooks/xiu.py | ||||||
|  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  |  | ||||||
|  |     (share filesystem-path srv/inc as volume /inc, | ||||||
|  |      readable by everyone, read-write for user 'ed', | ||||||
|  |      running this plugin on batches of uploads with the params listed below) | ||||||
|  |  | ||||||
|  | parameters explained, | ||||||
|  |     xiu = execute after uploads... | ||||||
|  |     i1  = ...after volume has been idle for 1sec | ||||||
|  |     j   = provide json instead of filepath list | ||||||
|  |  | ||||||
|  | note the "f" (fork) flag is not set, so this xiu | ||||||
|  | will block other xiu hooks while it's running | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     zb = sys.stdin.buffer.read() | ||||||
|  |     zs = zb.decode("utf-8", "replace") | ||||||
|  |     inf = json.loads(zs) | ||||||
|  |  | ||||||
|  |     total_sz = 0 | ||||||
|  |     for upload in inf: | ||||||
|  |         sz = upload["sz"] | ||||||
|  |         total_sz += sz | ||||||
|  |         print("{:9} {}".format(sz, upload["ap"])) | ||||||
|  |  | ||||||
|  |     print("{} files, {} bytes total".format(len(inf), total_sz)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
| @@ -1,10 +1,36 @@ | |||||||
| standalone programs which take an audio file as argument | standalone programs which take an audio file as argument | ||||||
|  |  | ||||||
|  | you may want to forget about all this fancy complicated stuff and just use [event hooks](../hooks/) instead (which doesn't need `-e2ts` or ffmpeg)  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | **NOTE:** these all require `-e2ts` to be functional, meaning you need to do at least one of these: `apt install ffmpeg` or `pip3 install mutagen` | ||||||
|  |  | ||||||
| some of these rely on libraries which are not MIT-compatible | some of these rely on libraries which are not MIT-compatible | ||||||
|  |  | ||||||
| * [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2 | * [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2 | ||||||
| * [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3 | * [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3 | ||||||
|  |  | ||||||
|  | these invoke standalone programs which are GPL or similar, so is legally fine for most purposes: | ||||||
|  |  | ||||||
| * [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL) | * [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL) | ||||||
|  | * [image-noexif.py](./image-noexif.py) removes exif tags from images; uses exiftool (GPLv1 or artistic-license) | ||||||
|  |  | ||||||
|  | these do not have any problematic dependencies at all: | ||||||
|  |  | ||||||
|  | * [cksum.py](./cksum.py) computes various checksums | ||||||
|  | * [exe.py](./exe.py) grabs metadata from .exe and .dll files (example for retrieving multiple tags with one parser) | ||||||
|  | * [wget.py](./wget.py) lets you download files by POSTing URLs to copyparty | ||||||
|  |   * also available as an [event hook](../hooks/wget.py) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## dangerous plugins | ||||||
|  |  | ||||||
|  | plugins in this section should only be used with appropriate precautions: | ||||||
|  |  | ||||||
|  | * [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone | ||||||
|  |   * also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control | ||||||
|  |   * anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN! | ||||||
|  |  | ||||||
|  |  | ||||||
| # dependencies | # dependencies | ||||||
| @@ -14,7 +40,7 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ | |||||||
| *alternatively* (or preferably) use packages from your distro instead, then you'll need at least these: | *alternatively* (or preferably) use packages from your distro instead, then you'll need at least these: | ||||||
|  |  | ||||||
| * from distro: `numpy vamp-plugin-sdk beatroot-vamp mixxx-keyfinder ffmpeg` | * from distro: `numpy vamp-plugin-sdk beatroot-vamp mixxx-keyfinder ffmpeg` | ||||||
| * from pypy: `keyfinder vamp` | * from pip: `keyfinder vamp` | ||||||
|  |  | ||||||
|  |  | ||||||
| # usage from copyparty | # usage from copyparty | ||||||
| @@ -30,7 +56,7 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ | |||||||
| * `mtp` modules will not run if a file has existing tags in the db, so clear out the tags with `-e2tsr` the first time you launch with new `mtp` options | * `mtp` modules will not run if a file has existing tags in the db, so clear out the tags with `-e2tsr` the first time you launch with new `mtp` options | ||||||
|  |  | ||||||
|  |  | ||||||
| ## usage with volume-flags | ## usage with volflags | ||||||
|  |  | ||||||
| instead of affecting all volumes, you can set the options for just one volume like so: | instead of affecting all volumes, you can set the options for just one volume like so: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,20 +16,24 @@ dep: ffmpeg | |||||||
| """ | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # save beat timestamps to ".beats/filename.txt" | ||||||
|  | SAVE = False | ||||||
|  |  | ||||||
|  |  | ||||||
| def det(tf): | def det(tf): | ||||||
|     # fmt: off |     # fmt: off | ||||||
|     sp.check_call([ |     sp.check_call([ | ||||||
|         "ffmpeg", |         b"ffmpeg", | ||||||
|         "-nostdin", |         b"-nostdin", | ||||||
|         "-hide_banner", |         b"-hide_banner", | ||||||
|         "-v", "fatal", |         b"-v", b"fatal", | ||||||
|         "-ss", "13", |         b"-y", b"-i", fsenc(sys.argv[1]), | ||||||
|         "-y", "-i", fsenc(sys.argv[1]), |         b"-map", b"0:a:0", | ||||||
|         "-ac", "1", |         b"-ac", b"1", | ||||||
|         "-ar", "22050", |         b"-ar", b"22050", | ||||||
|         "-t", "300", |         b"-t", b"360", | ||||||
|         "-f", "f32le", |         b"-f", b"f32le", | ||||||
|         tf |         fsenc(tf) | ||||||
|     ]) |     ]) | ||||||
|     # fmt: on |     # fmt: on | ||||||
|  |  | ||||||
| @@ -46,10 +50,29 @@ def det(tf): | |||||||
|             print(c["list"][0]["label"].split(" ")[0]) |             print(c["list"][0]["label"].split(" ")[0]) | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         # throws if detection failed: |     # throws if detection failed: | ||||||
|         bpm = float(cl[-1]["timestamp"] - cl[1]["timestamp"]) |     beats = [float(x["timestamp"]) for x in cl] | ||||||
|         bpm = round(60 * ((len(cl) - 1) / bpm), 2) |     bds = [b - a for a, b in zip(beats, beats[1:])] | ||||||
|         print(f"{bpm:.2f}") |     bds.sort() | ||||||
|  |     n0 = int(len(bds) * 0.2) | ||||||
|  |     n1 = int(len(bds) * 0.75) + 1 | ||||||
|  |     bds = bds[n0:n1] | ||||||
|  |     bpm = sum(bds) | ||||||
|  |     bpm = round(60 * (len(bds) / bpm), 2) | ||||||
|  |     print(f"{bpm:.2f}") | ||||||
|  |  | ||||||
|  |     if SAVE: | ||||||
|  |         fdir, fname = os.path.split(sys.argv[1]) | ||||||
|  |         bdir = os.path.join(fdir, ".beats") | ||||||
|  |         try: | ||||||
|  |             os.mkdir(fsenc(bdir)) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         fp = os.path.join(bdir, fname) + ".txt" | ||||||
|  |         with open(fsenc(fp), "wb") as f: | ||||||
|  |             txt = "\n".join([f"{x:.2f}" for x in beats]) | ||||||
|  |             f.write(txt.encode("utf-8")) | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|   | |||||||
| @@ -23,14 +23,15 @@ dep: ffmpeg | |||||||
| def det(tf): | def det(tf): | ||||||
|     # fmt: off |     # fmt: off | ||||||
|     sp.check_call([ |     sp.check_call([ | ||||||
|         "ffmpeg", |         b"ffmpeg", | ||||||
|         "-nostdin", |         b"-nostdin", | ||||||
|         "-hide_banner", |         b"-hide_banner", | ||||||
|         "-v", "fatal", |         b"-v", b"fatal", | ||||||
|         "-y", "-i", fsenc(sys.argv[1]), |         b"-y", b"-i", fsenc(sys.argv[1]), | ||||||
|         "-t", "300", |         b"-map", b"0:a:0", | ||||||
|         "-sample_fmt", "s16", |         b"-t", b"300", | ||||||
|         tf |         b"-sample_fmt", b"s16", | ||||||
|  |         fsenc(tf) | ||||||
|     ]) |     ]) | ||||||
|     # fmt: on |     # fmt: on | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								bin/mtag/cksum.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										89
									
								
								bin/mtag/cksum.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import zlib | ||||||
|  | import struct | ||||||
|  | import base64 | ||||||
|  | import hashlib | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | calculates various checksums for uploads, | ||||||
|  | usage: -mtp crc32,md5,sha1,sha256b=ad,bin/mtag/cksum.py | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     config = "crc32 md5 md5b sha1 sha1b sha256 sha256b sha512/240 sha512b/240" | ||||||
|  |     # b suffix = base64 encoded | ||||||
|  |     # slash = truncate to n bits | ||||||
|  |  | ||||||
|  |     known = { | ||||||
|  |         "md5": hashlib.md5, | ||||||
|  |         "sha1": hashlib.sha1, | ||||||
|  |         "sha256": hashlib.sha256, | ||||||
|  |         "sha512": hashlib.sha512, | ||||||
|  |     } | ||||||
|  |     config = config.split() | ||||||
|  |     hashers = { | ||||||
|  |         k: v() | ||||||
|  |         for k, v in known.items() | ||||||
|  |         if k in [x.split("/")[0].rstrip("b") for x in known] | ||||||
|  |     } | ||||||
|  |     crc32 = 0 if "crc32" in config else None | ||||||
|  |  | ||||||
|  |     with open(fsenc(sys.argv[1]), "rb", 512 * 1024) as f: | ||||||
|  |         while True: | ||||||
|  |             buf = f.read(64 * 1024) | ||||||
|  |             if not buf: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |             for x in hashers.values(): | ||||||
|  |                 x.update(buf) | ||||||
|  |  | ||||||
|  |             if crc32 is not None: | ||||||
|  |                 crc32 = zlib.crc32(buf, crc32) | ||||||
|  |  | ||||||
|  |     ret = {} | ||||||
|  |     for s in config: | ||||||
|  |         alg = s.split("/")[0] | ||||||
|  |         b64 = alg.endswith("b") | ||||||
|  |         alg = alg.rstrip("b") | ||||||
|  |         if alg in hashers: | ||||||
|  |             v = hashers[alg].digest() | ||||||
|  |         elif alg == "crc32": | ||||||
|  |             v = crc32 | ||||||
|  |             if v < 0: | ||||||
|  |                 v &= 2 ** 32 - 1 | ||||||
|  |             v = struct.pack(">L", v) | ||||||
|  |         else: | ||||||
|  |             raise Exception("what is {}".format(s)) | ||||||
|  |  | ||||||
|  |         if "/" in s: | ||||||
|  |             v = v[: int(int(s.split("/")[1]) / 8)] | ||||||
|  |  | ||||||
|  |         if b64: | ||||||
|  |             v = base64.b64encode(v).decode("ascii").rstrip("=") | ||||||
|  |         else: | ||||||
|  |             try: | ||||||
|  |                 v = v.hex() | ||||||
|  |             except: | ||||||
|  |                 import binascii | ||||||
|  |  | ||||||
|  |                 v = binascii.hexlify(v) | ||||||
|  |  | ||||||
|  |         ret[s] = v | ||||||
|  |  | ||||||
|  |     print(json.dumps(ret, indent=4)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										61
									
								
								bin/mtag/guestbook-read.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										61
									
								
								bin/mtag/guestbook-read.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | fetch latest msg from guestbook and return as tag | ||||||
|  |  | ||||||
|  | example copyparty config to use this: | ||||||
|  |   --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |   for realpath srv/hello (served at /hello), write-only for eveyrone, | ||||||
|  |   enable file analysis on upload (e2ts), | ||||||
|  |   use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook", | ||||||
|  |   do this on all uploads regardless of extension, | ||||||
|  |   t10 = 10 seconds timeout for each dwonload, | ||||||
|  |   ad = parse file regardless if FFmpeg thinks it is audio or not | ||||||
|  |   p = request upload info as json on stdin (need ip) | ||||||
|  |   mte=+guestbook enabled indexing of that tag for this volume | ||||||
|  |  | ||||||
|  | PS: this requires e2ts to be functional, | ||||||
|  |   meaning you need to do at least one of these: | ||||||
|  |    * apt install ffmpeg | ||||||
|  |    * pip3 install mutagen | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import sqlite3 | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # set 0 to allow infinite msgs from one IP, | ||||||
|  | # other values delete older messages to make space, | ||||||
|  | # so 1 only keeps latest msg | ||||||
|  | NUM_MSGS_TO_KEEP = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = os.path.abspath(sys.argv[1]) | ||||||
|  |     fdir = os.path.dirname(fp) | ||||||
|  |  | ||||||
|  |     zb = sys.stdin.buffer.read() | ||||||
|  |     zs = zb.decode("utf-8", "replace") | ||||||
|  |     md = json.loads(zs) | ||||||
|  |  | ||||||
|  |     ip = md["up_ip"] | ||||||
|  |  | ||||||
|  |     # can put the database inside `fdir` if you'd like, | ||||||
|  |     # by default it saves to PWD: | ||||||
|  |     # os.chdir(fdir) | ||||||
|  |  | ||||||
|  |     db = sqlite3.connect("guestbook.db3") | ||||||
|  |     with db: | ||||||
|  |         t = "select msg from gb where ip = ? order by ts desc" | ||||||
|  |         r = db.execute(t, (ip,)).fetchone() | ||||||
|  |         if r: | ||||||
|  |             print(r[0]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										111
									
								
								bin/mtag/guestbook.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								bin/mtag/guestbook.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | store messages from users in an sqlite database | ||||||
|  | which can be read from another mtp for example | ||||||
|  |  | ||||||
|  | takes input from application/x-www-form-urlencoded POSTs, | ||||||
|  | for example using the message/pager function on the website | ||||||
|  |  | ||||||
|  | example copyparty config to use this: | ||||||
|  |   --urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |   for realpath srv/hello (served at /hello),write-only for eveyrone, | ||||||
|  |   enable file analysis on upload (e2ts), | ||||||
|  |   use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb", | ||||||
|  |   do this on all uploads with the file extension "bin", | ||||||
|  |   t300 = 300 seconds timeout for each dwonload, | ||||||
|  |   ad = parse file regardless if FFmpeg thinks it is audio or not | ||||||
|  |   p = request upload info as json on stdin | ||||||
|  |   mte=+xgb enabled indexing of that tag for this volume | ||||||
|  |  | ||||||
|  | PS: this requires e2ts to be functional, | ||||||
|  |   meaning you need to do at least one of these: | ||||||
|  |    * apt install ffmpeg | ||||||
|  |    * pip3 install mutagen | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import sqlite3 | ||||||
|  | import sys | ||||||
|  | from urllib.parse import unquote_to_bytes as unquote | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # set 0 to allow infinite msgs from one IP, | ||||||
|  | # other values delete older messages to make space, | ||||||
|  | # so 1 only keeps latest msg | ||||||
|  | NUM_MSGS_TO_KEEP = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = os.path.abspath(sys.argv[1]) | ||||||
|  |     fdir = os.path.dirname(fp) | ||||||
|  |     fname = os.path.basename(fp) | ||||||
|  |     if not fname.startswith("put-") or not fname.endswith(".bin"): | ||||||
|  |         raise Exception("not a post file") | ||||||
|  |  | ||||||
|  |     zb = sys.stdin.buffer.read() | ||||||
|  |     zs = zb.decode("utf-8", "replace") | ||||||
|  |     md = json.loads(zs) | ||||||
|  |  | ||||||
|  |     buf = b"" | ||||||
|  |     with open(fp, "rb") as f: | ||||||
|  |         while True: | ||||||
|  |             b = f.read(4096) | ||||||
|  |             buf += b | ||||||
|  |             if len(buf) > 4096: | ||||||
|  |                 raise Exception("too big") | ||||||
|  |  | ||||||
|  |             if not b: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |     if not buf: | ||||||
|  |         raise Exception("file is empty") | ||||||
|  |  | ||||||
|  |     buf = unquote(buf.replace(b"+", b" ")) | ||||||
|  |     txt = buf.decode("utf-8") | ||||||
|  |  | ||||||
|  |     if not txt.startswith("msg="): | ||||||
|  |         raise Exception("does not start with msg=") | ||||||
|  |  | ||||||
|  |     ip = md["up_ip"] | ||||||
|  |     ts = md["up_at"] | ||||||
|  |     txt = txt[4:] | ||||||
|  |  | ||||||
|  |     # can put the database inside `fdir` if you'd like, | ||||||
|  |     # by default it saves to PWD: | ||||||
|  |     # os.chdir(fdir) | ||||||
|  |  | ||||||
|  |     db = sqlite3.connect("guestbook.db3") | ||||||
|  |     try: | ||||||
|  |         db.execute("select 1 from gb").fetchone() | ||||||
|  |     except: | ||||||
|  |         with db: | ||||||
|  |             db.execute("create table gb (ip text, ts real, msg text)") | ||||||
|  |             db.execute("create index gb_ip on gb(ip)") | ||||||
|  |  | ||||||
|  |     with db: | ||||||
|  |         if NUM_MSGS_TO_KEEP == 1: | ||||||
|  |             t = "delete from gb where ip = ?" | ||||||
|  |             db.execute(t, (ip,)) | ||||||
|  |  | ||||||
|  |         t = "insert into gb values (?,?,?)" | ||||||
|  |         db.execute(t, (ip, ts, txt)) | ||||||
|  |  | ||||||
|  |         if NUM_MSGS_TO_KEEP > 1: | ||||||
|  |             t = "select ts from gb where ip = ? order by ts desc" | ||||||
|  |             hits = db.execute(t, (ip,)).fetchall() | ||||||
|  |  | ||||||
|  |             if len(hits) > NUM_MSGS_TO_KEEP: | ||||||
|  |                 lim = hits[NUM_MSGS_TO_KEEP][0] | ||||||
|  |                 t = "delete from gb where ip = ? and ts <= ?" | ||||||
|  |                 db.execute(t, (ip, lim)) | ||||||
|  |  | ||||||
|  |     print(txt) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										95
									
								
								bin/mtag/image-noexif.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								bin/mtag/image-noexif.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | remove exif tags from uploaded images | ||||||
|  |  | ||||||
|  | dependencies: | ||||||
|  |   exiftool | ||||||
|  |  | ||||||
|  | about: | ||||||
|  |   creates a "noexif" subfolder and puts exif-stripped copies of each image there, | ||||||
|  |   the reason for the subfolder is to avoid issues with the up2k.db / deduplication: | ||||||
|  |  | ||||||
|  |   if the original image is modified in-place, then copyparty will keep the original | ||||||
|  |   hash in up2k.db for a while (until the next volume rescan), so if the image is | ||||||
|  |   reuploaded after a rescan then the upload will be renamed and kept as a dupe | ||||||
|  |  | ||||||
|  |   alternatively you could switch the logic around, making a copy of the original | ||||||
|  |   image into a subfolder named "exif" and modify the original in-place, but then | ||||||
|  |   up2k.db will be out of sync until the next rescan, so any additional uploads | ||||||
|  |   of the same image will get symlinked (deduplicated) to the modified copy | ||||||
|  |   instead of the original in "exif" | ||||||
|  |  | ||||||
|  |   or maybe delete the original image after processing, that would kinda work too | ||||||
|  |  | ||||||
|  | example copyparty config to use this: | ||||||
|  |   -v/mnt/nas/pics:pics:rwmd,ed:c,e2ts,mte=+noexif:c,mtp=noexif=ejpg,ejpeg,ad,bin/mtag/image-noexif.py | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |   for realpath /mnt/nas/pics (served at /pics) with read-write-modify-delete for ed, | ||||||
|  |   enable file analysis on upload (e2ts), | ||||||
|  |   append "noexif" to the list of known tags (mtp), | ||||||
|  |   and use mtp plugin "bin/mtag/image-noexif.py" to provide that tag, | ||||||
|  |   do this on all uploads with the file extension "jpg" or "jpeg", | ||||||
|  |   ad = parse file regardless if FFmpeg thinks it is audio or not | ||||||
|  |  | ||||||
|  | PS: this requires e2ts to be functional, | ||||||
|  |   meaning you need to do at least one of these: | ||||||
|  |    * apt install ffmpeg | ||||||
|  |    * pip3 install mutagen | ||||||
|  |   and your python must have sqlite3 support compiled in | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import filecmp | ||||||
|  | import subprocess as sp | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p.encode("utf-8") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     cwd, fn = os.path.split(sys.argv[1]) | ||||||
|  |     if os.path.basename(cwd) == "noexif": | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     os.chdir(cwd) | ||||||
|  |     f1 = fsenc(fn) | ||||||
|  |     f2 = fsenc(os.path.join(b"noexif", fn)) | ||||||
|  |     cmd = [ | ||||||
|  |         b"exiftool", | ||||||
|  |         b"-exif:all=", | ||||||
|  |         b"-iptc:all=", | ||||||
|  |         b"-xmp:all=", | ||||||
|  |         b"-P", | ||||||
|  |         b"-o", | ||||||
|  |         b"noexif/", | ||||||
|  |         b"--", | ||||||
|  |         f1, | ||||||
|  |     ] | ||||||
|  |     sp.check_output(cmd) | ||||||
|  |     if not os.path.exists(f2): | ||||||
|  |         print("failed") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     if filecmp.cmp(f1, f2, shallow=False): | ||||||
|  |         print("clean") | ||||||
|  |     else: | ||||||
|  |         print("exif") | ||||||
|  |  | ||||||
|  |     # lastmod = os.path.getmtime(f1) | ||||||
|  |     # times = (int(time.time()), int(lastmod)) | ||||||
|  |     # os.utime(f2, times) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     try: | ||||||
|  |         main() | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
| @@ -4,7 +4,9 @@ set -e | |||||||
|  |  | ||||||
| # install dependencies for audio-*.py | # install dependencies for audio-*.py | ||||||
| # | # | ||||||
| # linux: requires {python3,ffmpeg,fftw}-dev py3-{wheel,pip} py3-numpy{,-dev} vamp-sdk-dev patchelf | # linux/alpine: requires gcc g++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-dev py3-{wheel,pip} py3-numpy{,-dev} | ||||||
|  | # linux/debian: requires libav{codec,device,filter,format,resample,util}-dev {libfftw3,python3,libsndfile1}-dev python3-{numpy,pip} vamp-{plugin-sdk,examples} patchelf cmake | ||||||
|  | # linux/fedora: requires gcc gcc-c++ make cmake patchelf {python3,ffmpeg,fftw,libsndfile}-devel python3-numpy vamp-plugin-sdk qm-vamp-plugins | ||||||
| # win64: requires msys2-mingw64 environment | # win64: requires msys2-mingw64 environment | ||||||
| # macos: requires macports | # macos: requires macports | ||||||
| # | # | ||||||
| @@ -55,6 +57,7 @@ hash -r | |||||||
| 	command -v python3 && pybin=python3 || pybin=python | 	command -v python3 && pybin=python3 || pybin=python | ||||||
| } | } | ||||||
|  |  | ||||||
|  | $pybin -c 'import numpy' || | ||||||
| $pybin -m pip install --user numpy | $pybin -m pip install --user numpy | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -100,8 +103,11 @@ export -f dl_files | |||||||
|  |  | ||||||
|  |  | ||||||
| github_tarball() { | github_tarball() { | ||||||
|  | 	rm -rf g | ||||||
|  | 	mkdir g | ||||||
|  | 	cd g | ||||||
| 	dl_text "$1" | | 	dl_text "$1" | | ||||||
| 	tee json | | 	tee ../json | | ||||||
| 	( | 	( | ||||||
| 		# prefer jq if available | 		# prefer jq if available | ||||||
| 		jq -r '.tarball_url' || | 		jq -r '.tarball_url' || | ||||||
| @@ -110,8 +116,11 @@ github_tarball() { | |||||||
| 		awk -F\" '/"tarball_url": "/ {print$4}' | 		awk -F\" '/"tarball_url": "/ {print$4}' | ||||||
| 	) | | 	) | | ||||||
| 	tee /dev/stderr | | 	tee /dev/stderr | | ||||||
|  | 	head -n 1 | | ||||||
| 	tr -d '\r' | tr '\n' '\0' | | 	tr -d '\r' | tr '\n' '\0' | | ||||||
| 	xargs -0 bash -c 'dl_files "$@"' _ | 	xargs -0 bash -c 'dl_files "$@"' _ | ||||||
|  | 	mv * ../tgz | ||||||
|  | 	cd .. | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -126,6 +135,7 @@ gitlab_tarball() { | |||||||
| 		tr \" '\n' | grep -E '\.tar\.gz$' | head -n 1 | 		tr \" '\n' | grep -E '\.tar\.gz$' | head -n 1 | ||||||
| 	) | | 	) | | ||||||
| 	tee /dev/stderr | | 	tee /dev/stderr | | ||||||
|  | 	head -n 1 | | ||||||
| 	tr -d '\r' | tr '\n' '\0' | | 	tr -d '\r' | tr '\n' '\0' | | ||||||
| 	tee links | | 	tee links | | ||||||
| 	xargs -0 bash -c 'dl_files "$@"' _ | 	xargs -0 bash -c 'dl_files "$@"' _ | ||||||
| @@ -137,20 +147,27 @@ install_keyfinder() { | |||||||
| 	#   use msys2 in mingw-w64 mode | 	#   use msys2 in mingw-w64 mode | ||||||
| 	#   pacman -S --needed mingw-w64-x86_64-{ffmpeg,python} | 	#   pacman -S --needed mingw-w64-x86_64-{ffmpeg,python} | ||||||
| 	 | 	 | ||||||
| 	github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest | 	[ -e $HOME/pe/keyfinder ] && { | ||||||
|  | 		echo found a keyfinder build in ~/pe, skipping | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	tar -xf mixxxdj-libkeyfinder-* | 	cd "$td" | ||||||
| 	rm -- *.tar.gz | 	github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest | ||||||
|  | 	ls -al | ||||||
|  |  | ||||||
|  | 	tar -xf tgz | ||||||
|  | 	rm tgz | ||||||
| 	cd mixxxdj-libkeyfinder* | 	cd mixxxdj-libkeyfinder* | ||||||
| 	 | 	 | ||||||
| 	h="$HOME" | 	h="$HOME" | ||||||
| 	so="lib/libkeyfinder.so" | 	so="lib/libkeyfinder.so" | ||||||
| 	memes=() | 	memes=(-DBUILD_TESTING=OFF) | ||||||
|  |  | ||||||
| 	[ $win ] && | 	[ $win ] && | ||||||
| 		so="bin/libkeyfinder.dll" && | 		so="bin/libkeyfinder.dll" && | ||||||
| 		h="$(printf '%s\n' "$USERPROFILE" | tr '\\' '/')" && | 		h="$(printf '%s\n' "$USERPROFILE" | tr '\\' '/')" && | ||||||
| 		memes+=(-G "MinGW Makefiles" -DBUILD_TESTING=OFF) | 		memes+=(-G "MinGW Makefiles") | ||||||
| 	 | 	 | ||||||
| 	[ $mac ] && | 	[ $mac ] && | ||||||
| 		so="lib/libkeyfinder.dylib" | 		so="lib/libkeyfinder.dylib" | ||||||
| @@ -170,7 +187,7 @@ install_keyfinder() { | |||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder* | 	# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder* | ||||||
| 	CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include" \ | 	CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include -I/usr/include/ffmpeg" \ | ||||||
| 	LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \ | 	LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \ | ||||||
| 	PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \ | 	PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \ | ||||||
| 	$pybin -m pip install --user keyfinder | 	$pybin -m pip install --user keyfinder | ||||||
| @@ -207,6 +224,22 @@ install_vamp() { | |||||||
| 	 | 	 | ||||||
| 	$pybin -m pip install --user vamp | 	$pybin -m pip install --user vamp | ||||||
|  |  | ||||||
|  | 	cd "$td" | ||||||
|  | 	echo '#include <vamp-sdk/Plugin.h>' | g++ -x c++ -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || { | ||||||
|  | 		printf '\033[33mcould not find the vamp-sdk, building from source\033[0m\n' | ||||||
|  | 		(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/2588/vamp-plugin-sdk-2.9.0.tar.gz) | ||||||
|  | 		sha512sum -c <( | ||||||
|  | 			echo "7ef7f837d19a08048b059e0da408373a7964ced452b290fae40b85d6d70ca9000bcfb3302cd0b4dc76cf2a848528456f78c1ce1ee0c402228d812bd347b6983b  -" | ||||||
|  | 		) <vamp-plugin-sdk-2.9.0.tar.gz | ||||||
|  | 		tar -xf vamp-plugin-sdk-2.9.0.tar.gz | ||||||
|  | 		rm -- *.tar.gz | ||||||
|  | 		ls -al | ||||||
|  | 		cd vamp-plugin-sdk-* | ||||||
|  | 		./configure --prefix=$HOME/pe/vamp-sdk | ||||||
|  | 		make -j1 install | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cd "$td" | ||||||
| 	have_beatroot || { | 	have_beatroot || { | ||||||
| 		printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n' | 		printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n' | ||||||
| 		(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/885/beatroot-vamp-v1.0.tar.gz) | 		(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/885/beatroot-vamp-v1.0.tar.gz) | ||||||
| @@ -214,8 +247,12 @@ install_vamp() { | |||||||
| 			echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874  -" | 			echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874  -" | ||||||
| 		) <beatroot-vamp-v1.0.tar.gz | 		) <beatroot-vamp-v1.0.tar.gz | ||||||
| 		tar -xf beatroot-vamp-v1.0.tar.gz  | 		tar -xf beatroot-vamp-v1.0.tar.gz  | ||||||
|  | 		rm -- *.tar.gz | ||||||
| 		cd beatroot-vamp-v1.0 | 		cd beatroot-vamp-v1.0 | ||||||
| 		make -f Makefile.linux -j4 | 		[ -e ~/pe/vamp-sdk ] && | ||||||
|  | 			sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux || | ||||||
|  | 			sed -ri 's`^(CFLAGS :=.*)`\1 -I/usr/include/vamp-sdk`' Makefile.linux | ||||||
|  | 		make -f Makefile.linux -j4 LDFLAGS="-L$HOME/pe/vamp-sdk/lib -L/usr/lib64" | ||||||
| 		# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp | 		# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp | ||||||
| 		mkdir ~/vamp | 		mkdir ~/vamp | ||||||
| 		cp -pv beatroot-vamp.* ~/vamp/ | 		cp -pv beatroot-vamp.* ~/vamp/ | ||||||
| @@ -229,6 +266,7 @@ install_vamp() { | |||||||
|  |  | ||||||
| # not in use because it kinda segfaults, also no windows support | # not in use because it kinda segfaults, also no windows support | ||||||
| install_soundtouch() { | install_soundtouch() { | ||||||
|  | 	cd "$td" | ||||||
| 	gitlab_tarball https://gitlab.com/api/v4/projects/soundtouch%2Fsoundtouch/releases | 	gitlab_tarball https://gitlab.com/api/v4/projects/soundtouch%2Fsoundtouch/releases | ||||||
| 	 | 	 | ||||||
| 	tar -xvf soundtouch-* | 	tar -xvf soundtouch-* | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ try: | |||||||
| except: | except: | ||||||
|  |  | ||||||
|     def fsenc(p): |     def fsenc(p): | ||||||
|         return p |         return p.encode("utf-8") | ||||||
|  |  | ||||||
|  |  | ||||||
| """ | """ | ||||||
| @@ -24,13 +24,13 @@ dep: ffmpeg | |||||||
| def det(): | def det(): | ||||||
|     # fmt: off |     # fmt: off | ||||||
|     cmd = [ |     cmd = [ | ||||||
|         "ffmpeg", |         b"ffmpeg", | ||||||
|         "-nostdin", |         b"-nostdin", | ||||||
|         "-hide_banner", |         b"-hide_banner", | ||||||
|         "-v", "fatal", |         b"-v", b"fatal", | ||||||
|         "-i", fsenc(sys.argv[1]), |         b"-i", fsenc(sys.argv[1]), | ||||||
|         "-f", "framemd5", |         b"-f", b"framemd5", | ||||||
|         "-" |         b"-" | ||||||
|     ] |     ] | ||||||
|     # fmt: on |     # fmt: on | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								bin/mtag/mousepad.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								bin/mtag/mousepad.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | mtp test -- opens a texteditor | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   -vsrv/v1:v1:r:c,mte=+x1:c,mtp=x1=ad,p,bin/mtag/mousepad.py | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |   c,mte: list of tags to index in this volume | ||||||
|  |   c,mtp: add new tag provider | ||||||
|  |      x1: dummy tag to provide | ||||||
|  |      ad: dontcare if audio or not | ||||||
|  |       p: priority 1 (run after initial tag-scan with ffprobe or mutagen) | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     env = os.environ.copy() | ||||||
|  |     env["DISPLAY"] = ":0.0" | ||||||
|  |  | ||||||
|  |     if False: | ||||||
|  |         # open the uploaded file | ||||||
|  |         fp = sys.argv[-1] | ||||||
|  |     else: | ||||||
|  |         # display stdin contents (`oth_tags`) | ||||||
|  |         fp = "/dev/stdin" | ||||||
|  |  | ||||||
|  |     p = sp.Popen(["/usr/bin/mousepad", fp]) | ||||||
|  |     p.communicate() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | main() | ||||||
							
								
								
									
										76
									
								
								bin/mtag/rclone-upload.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								bin/mtag/rclone-upload.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import subprocess as sp | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p.encode("utf-8") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | first checks the tag "vidchk" which must be "ok" to continue, | ||||||
|  | then uploads all files to some cloud storage (RCLONE_REMOTE) | ||||||
|  | and DELETES THE ORIGINAL FILES if rclone returns 0 ("success") | ||||||
|  |  | ||||||
|  | deps: | ||||||
|  |   rclone | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   -mtp x2=t43200,ay,p2,bin/mtag/rclone-upload.py | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  | t43200: timeout 12h | ||||||
|  |     ay: only process files which contain audio (including video with audio) | ||||||
|  |     p2: set priority 2 (after vidchk's suggested priority of 1), | ||||||
|  |           so the output of vidchk will be passed in here | ||||||
|  |  | ||||||
|  | complete usage example as vflags along with vidchk: | ||||||
|  |   -vsrv/vidchk:vidchk:r:rw,ed:c,e2dsa,e2ts,mtp=vidchk=t600,p,bin/mtag/vidchk.py:c,mtp=rupload=t43200,ay,p2,bin/mtag/rclone-upload.py:c,mte=+vidchk,rupload | ||||||
|  |  | ||||||
|  | setup: see https://rclone.org/drive/ | ||||||
|  |  | ||||||
|  | if you wanna use this script standalone / separately from copyparty, | ||||||
|  | either set CONDITIONAL_UPLOAD False or provide the following stdin: | ||||||
|  |   {"vidchk":"ok"} | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | RCLONE_REMOTE = "notmybox" | ||||||
|  | CONDITIONAL_UPLOAD = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = sys.argv[1] | ||||||
|  |     if CONDITIONAL_UPLOAD: | ||||||
|  |         zb = sys.stdin.buffer.read() | ||||||
|  |         zs = zb.decode("utf-8", "replace") | ||||||
|  |         md = json.loads(zs) | ||||||
|  |  | ||||||
|  |         chk = md.get("vidchk", None) | ||||||
|  |         if chk != "ok": | ||||||
|  |             print(f"vidchk={chk}", file=sys.stderr) | ||||||
|  |             sys.exit(1) | ||||||
|  |  | ||||||
|  |     dst = f"{RCLONE_REMOTE}:".encode("utf-8") | ||||||
|  |     cmd = [b"rclone", b"copy", b"--", fsenc(fp), dst] | ||||||
|  |  | ||||||
|  |     t0 = time.time() | ||||||
|  |     try: | ||||||
|  |         sp.check_call(cmd) | ||||||
|  |     except: | ||||||
|  |         print("rclone failed", file=sys.stderr) | ||||||
|  |         sys.exit(1) | ||||||
|  |  | ||||||
|  |     print(f"{time.time() - t0:.1f} sec") | ||||||
|  |     os.unlink(fsenc(fp)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										21
									
								
								bin/mtag/res/twitter-unmute.user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								bin/mtag/res/twitter-unmute.user.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | // ==UserScript== | ||||||
|  | // @name         twitter-unmute | ||||||
|  | // @namespace    http://ocv.me/ | ||||||
|  | // @version      0.1 | ||||||
|  | // @description  memes | ||||||
|  | // @author       ed <irc.rizon.net> | ||||||
|  | // @match        https://twitter.com/* | ||||||
|  | // @icon         https://www.google.com/s2/favicons?domain=twitter.com | ||||||
|  | // @grant        GM_addStyle | ||||||
|  | // ==/UserScript== | ||||||
|  |  | ||||||
|  | function grunnur() { | ||||||
|  |     setInterval(function () { | ||||||
|  |         //document.querySelector('div[aria-label="Unmute"]').click(); | ||||||
|  |         document.querySelector('video').muted = false; | ||||||
|  |     }, 200); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var scr = document.createElement('script'); | ||||||
|  | scr.textContent = '(' + grunnur.toString() + ')();'; | ||||||
|  | (document.head || document.getElementsByTagName('head')[0]).appendChild(scr); | ||||||
							
								
								
									
										39
									
								
								bin/mtag/res/yt-ipr.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								bin/mtag/res/yt-ipr.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | # example config file to use copyparty as a youtube manifest collector, | ||||||
|  | # use with copyparty like:  python copyparty.py -c yt-ipr.conf | ||||||
|  | # | ||||||
|  | # see docs/example.conf for a better explanation of the syntax, but | ||||||
|  | # newlines are block separators, so adding blank lines inside a volume definition is bad | ||||||
|  | # (use comments as separators instead) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # create user ed, password wark | ||||||
|  | u ed:wark | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # create a volume at /ytm which stores files at ./srv/ytm | ||||||
|  | ./srv/ytm | ||||||
|  | /ytm | ||||||
|  | # write-only, but read-write for user ed | ||||||
|  | w | ||||||
|  | rw ed | ||||||
|  | # rescan the volume on startup | ||||||
|  | c e2dsa | ||||||
|  | # collect tags from all new files since last scan | ||||||
|  | c e2ts | ||||||
|  | # optionally enable compression to make the files 50% smaller | ||||||
|  | c pk | ||||||
|  | # only allow uploads which are between 16k and 1m large | ||||||
|  | c sz=16k-1m | ||||||
|  | # allow up to 10 uploads over 5 minutes from each ip | ||||||
|  | c maxn=10,300 | ||||||
|  | # move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload> | ||||||
|  | c rotf=%Y-%m/%d-%H | ||||||
|  | # delete uploads when they are 24 hours old | ||||||
|  | c lifetime=86400 | ||||||
|  | # add the parser and tell copyparty what tags it can expect from it | ||||||
|  | c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py | ||||||
|  | # decide which tags we want to index and in what order | ||||||
|  | c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # create any other volumes you'd like down here, or merge this with an existing config file | ||||||
| @@ -7,26 +7,41 @@ | |||||||
| // ==/UserScript== | // ==/UserScript== | ||||||
|  |  | ||||||
| function main() { | function main() { | ||||||
|  |     var server = 'https://127.0.0.1:3923/ytm?pw=wark', | ||||||
|  |         interval = 60; // sec | ||||||
|  |  | ||||||
|     var sent = {}; |     var sent = {}; | ||||||
|     function send(txt) { |     function send(txt, mf_url, desc) { | ||||||
|         if (sent[txt]) |         if (sent[mf_url]) | ||||||
|             return; |             return; | ||||||
|  |  | ||||||
|         fetch('https://127.0.0.1:3923/playerdata?_=' + Date.now(), { method: "PUT", body: txt }); |         fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt }); | ||||||
|         console.log('[yt-ipr] yeet %d bytes', txt.length); |         console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc); | ||||||
|         sent[txt] = 1; |         sent[mf_url] = 1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function collect() { |     function collect() { | ||||||
|         setTimeout(collect, 60 * 1000); |         try { | ||||||
|         var pd = document.querySelector('ytd-watch-flexy'); |             var pd = document.querySelector('ytd-watch-flexy'); | ||||||
|         if (pd) |             if (!pd) | ||||||
|             send(JSON.stringify(pd.playerData)); |                 return console.log('[yt-pdh] no video found'); | ||||||
|  |  | ||||||
|  |             pd = pd.playerData; | ||||||
|  |             var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl; | ||||||
|  |             if (!mu || !mu.length) | ||||||
|  |                 return console.log('[yt-pdh] no manifest found'); | ||||||
|  |  | ||||||
|  |             var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title; | ||||||
|  |             send(JSON.stringify(pd), mu, desc); | ||||||
|  |         } | ||||||
|  |         catch (ex) { | ||||||
|  |             console.log("[yt-pdh]", ex); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     setTimeout(collect, 5000); |     setInterval(collect, interval * 1000); | ||||||
| } | } | ||||||
|  |  | ||||||
| var scr = document.createElement('script'); | var scr = document.createElement('script'); | ||||||
| scr.textContent = '(' + main.toString() + ')();'; | scr.textContent = '(' + main.toString() + ')();'; | ||||||
| (document.head || document.getElementsByTagName('head')[0]).appendChild(scr); | (document.head || document.getElementsByTagName('head')[0]).appendChild(scr); | ||||||
| console.log('[yt-ipr] a'); | console.log('[yt-pdh] a'); | ||||||
|   | |||||||
							
								
								
									
										205
									
								
								bin/mtag/very-bad-idea.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										205
									
								
								bin/mtag/very-bad-idea.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,205 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | WARNING -- DANGEROUS PLUGIN -- | ||||||
|  |   if someone is able to upload files to a copyparty which is | ||||||
|  |   running this plugin, they can execute malware on your machine | ||||||
|  |   so please keep this on a LAN and protect it with a password | ||||||
|  |  | ||||||
|  | use copyparty as a chromecast replacement: | ||||||
|  |   * post a URL and it will open in the default browser | ||||||
|  |   * upload a file and it will open in the default application | ||||||
|  |   * the `key` command simulates keyboard input | ||||||
|  |   * the `x` command executes other xdotool commands | ||||||
|  |   * the `c` command executes arbitrary unix commands | ||||||
|  |  | ||||||
|  | the android app makes it a breeze to post pics and links: | ||||||
|  |   https://github.com/9001/party-up/releases | ||||||
|  |  | ||||||
|  | iOS devices can use the web-UI or the shortcut instead: | ||||||
|  |   https://github.com/9001/copyparty#ios-shortcuts | ||||||
|  |  | ||||||
|  | example copyparty config to use this; | ||||||
|  | lets the user "kevin" with password "hunter2" use this plugin: | ||||||
|  |   -a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py | ||||||
|  |  | ||||||
|  | recommended deps: | ||||||
|  |   apt install xdotool libnotify-bin mpv | ||||||
|  |   python3 -m pip install --user -U streamlink yt-dlp | ||||||
|  |   https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js | ||||||
|  |  | ||||||
|  | and you probably want `twitter-unmute.user.js` from the res folder | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ----------------------------------------------------------------------- | ||||||
|  | -- startup script: | ||||||
|  | ----------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # create qr code | ||||||
|  | ip=$(ip r | awk '/^default/{print$(NF-2)}'); echo http://$ip:3923/ | qrencode -o - -s 4 >/dev/shm/cpp-qr.png | ||||||
|  | /usr/bin/feh -x /dev/shm/cpp-qr.png & | ||||||
|  |  | ||||||
|  | # reposition and make topmost (with janky raspbian support) | ||||||
|  | ( sleep 0.5 | ||||||
|  | xdotool search --name cpp-qr.png windowactivate --sync windowmove 1780 0 | ||||||
|  | wmctrl -r :ACTIVE: -b toggle,above || true | ||||||
|  |  | ||||||
|  | ps aux | grep -E 'sleep[ ]7\.27' || | ||||||
|  | while true; do | ||||||
|  |   w=$(xdotool getactivewindow) | ||||||
|  |   xdotool search --name cpp-qr.png windowactivate windowraise windowfocus | ||||||
|  |   xdotool windowactivate $w | ||||||
|  |   xdotool windowfocus $w | ||||||
|  |   sleep 7.27 || break | ||||||
|  | done & | ||||||
|  | xeyes  # distraction window to prevent ^w from closing the qr-code | ||||||
|  | ) & | ||||||
|  |  | ||||||
|  | # bail if copyparty is already running | ||||||
|  | ps aux | grep -E '[3] copy[p]arty' && exit 0 | ||||||
|  |  | ||||||
|  | # dumb chrome wrapper to allow autoplay | ||||||
|  | cat >/usr/local/bin/chromium-browser <<'EOF' | ||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  | /usr/bin/chromium-browser --autoplay-policy=no-user-gesture-required "$@" | ||||||
|  | EOF | ||||||
|  | chmod 755 /usr/local/bin/chromium-browser | ||||||
|  |  | ||||||
|  | # start the server | ||||||
|  | # note 1: replace hunter2 with a better password to access the server | ||||||
|  | # note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff | ||||||
|  | cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py | ||||||
|  |  | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  | import shutil | ||||||
|  | import subprocess as sp | ||||||
|  | from urllib.parse import unquote_to_bytes as unquote | ||||||
|  | from urllib.parse import quote | ||||||
|  |  | ||||||
|  | have_mpv = shutil.which("mpv") | ||||||
|  | have_vlc = shutil.which("vlc") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     if len(sys.argv) > 2 and sys.argv[1] == "x": | ||||||
|  |         # invoked on commandline for testing; | ||||||
|  |         # python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ | ||||||
|  |         txt = " ".join(sys.argv[2:]) | ||||||
|  |         txt = quote(txt.replace(" ", "+")) | ||||||
|  |         return open_post(txt.encode("utf-8")) | ||||||
|  |  | ||||||
|  |     fp = os.path.abspath(sys.argv[1]) | ||||||
|  |     with open(fp, "rb") as f: | ||||||
|  |         txt = f.read(4096) | ||||||
|  |  | ||||||
|  |     if txt.startswith(b"msg="): | ||||||
|  |         open_post(txt) | ||||||
|  |     else: | ||||||
|  |         open_url(fp) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def open_post(txt): | ||||||
|  |     txt = unquote(txt.replace(b"+", b" ")).decode("utf-8")[4:] | ||||||
|  |     try: | ||||||
|  |         k, v = txt.split(" ", 1) | ||||||
|  |     except: | ||||||
|  |         return open_url(txt) | ||||||
|  |  | ||||||
|  |     if k == "key": | ||||||
|  |         sp.call(["xdotool", "key"] + v.split(" ")) | ||||||
|  |     elif k == "x": | ||||||
|  |         sp.call(["xdotool"] + v.split(" ")) | ||||||
|  |     elif k == "c": | ||||||
|  |         env = os.environ.copy() | ||||||
|  |         while " " in v: | ||||||
|  |             v1, v2 = v.split(" ", 1) | ||||||
|  |             if "=" not in v1: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |             ek, ev = v1.split("=", 1) | ||||||
|  |             env[ek] = ev | ||||||
|  |             v = v2 | ||||||
|  |  | ||||||
|  |         sp.call(v.split(" "), env=env) | ||||||
|  |     else: | ||||||
|  |         open_url(txt) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def open_url(txt): | ||||||
|  |     ext = txt.rsplit(".")[-1].lower() | ||||||
|  |     sp.call(["notify-send", "--", txt]) | ||||||
|  |     if ext not in ["jpg", "jpeg", "png", "gif", "webp"]: | ||||||
|  |         # sp.call(["wmctrl", "-c", ":ACTIVE:"])  # closes the active window correctly | ||||||
|  |         sp.call(["killall", "vlc"]) | ||||||
|  |         sp.call(["killall", "mpv"]) | ||||||
|  |         sp.call(["killall", "feh"]) | ||||||
|  |         time.sleep(0.5) | ||||||
|  |         for _ in range(20): | ||||||
|  |             sp.call(["xdotool", "key", "ctrl+w"])  # closes the open tab correctly | ||||||
|  |     # else: | ||||||
|  |     #    sp.call(["xdotool", "getactivewindow", "windowminimize"])  # minimizes the focused windo | ||||||
|  |  | ||||||
|  |     # mpv is probably smart enough to use streamlink automatically | ||||||
|  |     if try_mpv(txt): | ||||||
|  |         print("mpv got it") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     # or maybe streamlink would be a good choice to open this | ||||||
|  |     if try_streamlink(txt): | ||||||
|  |         print("streamlink got it") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     # nope, | ||||||
|  |     # close any error messages: | ||||||
|  |     sp.call(["xdotool", "search", "--name", "Error", "windowclose"]) | ||||||
|  |     # sp.call(["xdotool", "key", "ctrl+alt+d"])  # doesnt work at all | ||||||
|  |     # sp.call(["xdotool", "keydown", "--delay", "100", "ctrl+alt+d"]) | ||||||
|  |     # sp.call(["xdotool", "keyup", "ctrl+alt+d"]) | ||||||
|  |     sp.call(["xdg-open", txt]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def try_mpv(url): | ||||||
|  |     t0 = time.time() | ||||||
|  |     try: | ||||||
|  |         print("trying mpv...") | ||||||
|  |         sp.check_call(["mpv", "--fs", url]) | ||||||
|  |         return True | ||||||
|  |     except: | ||||||
|  |         # if it ran for 15 sec it probably succeeded and terminated | ||||||
|  |         t = time.time() | ||||||
|  |         return t - t0 > 15 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def try_streamlink(url): | ||||||
|  |     t0 = time.time() | ||||||
|  |     try: | ||||||
|  |         import streamlink | ||||||
|  |  | ||||||
|  |         print("trying streamlink...") | ||||||
|  |         streamlink.Streamlink().resolve_url(url) | ||||||
|  |  | ||||||
|  |         if have_mpv: | ||||||
|  |             args = "-m streamlink -p mpv -a --fs" | ||||||
|  |         else: | ||||||
|  |             args = "-m streamlink" | ||||||
|  |  | ||||||
|  |         cmd = [sys.executable] + args.split() + [url, "best"] | ||||||
|  |         t0 = time.time() | ||||||
|  |         sp.check_call(cmd) | ||||||
|  |         return True | ||||||
|  |     except: | ||||||
|  |         # if it ran for 10 sec it probably succeeded and terminated | ||||||
|  |         t = time.time() | ||||||
|  |         return t - t0 > 10 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | main() | ||||||
							
								
								
									
										131
									
								
								bin/mtag/vidchk.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										131
									
								
								bin/mtag/vidchk.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,131 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | import re | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from copyparty.util import fsenc | ||||||
|  | except: | ||||||
|  |  | ||||||
|  |     def fsenc(p): | ||||||
|  |         return p.encode("utf-8") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _ = r""" | ||||||
|  | inspects video files for errors and such | ||||||
|  | plus stores a bunch of metadata to filename.ff.json | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   -mtp vidchk=t600,ay,p,bin/mtag/vidchk.py | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  | t600: timeout 10min | ||||||
|  |   ay: only process files which contain audio (including video with audio) | ||||||
|  |    p: set priority 1 (lowest priority after initial ffprobe/mutagen for base tags), | ||||||
|  |        makes copyparty feed base tags into this script as json | ||||||
|  |  | ||||||
|  | if you wanna use this script standalone / separately from copyparty, | ||||||
|  | provide the video resolution on stdin as json:  {"res":"1920x1080"} | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FAST = True  # parse entire file at container level | ||||||
|  | # FAST = False  # fully decode audio and video streams | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # warnings to ignore | ||||||
|  | harmless = re.compile( | ||||||
|  |     r"Unsupported codec with id |Could not find codec parameters.*Attachment:|analyzeduration" | ||||||
|  |     + r"|timescale not set" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def wfilter(lines): | ||||||
|  |     return [x for x in lines if x.strip() and not harmless.search(x)] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def errchk(so, se, rc, dbg): | ||||||
|  |     if dbg: | ||||||
|  |         with open(dbg, "wb") as f: | ||||||
|  |             f.write(b"so:\n" + so + b"\nse:\n" + se + b"\n") | ||||||
|  |  | ||||||
|  |     if rc: | ||||||
|  |         err = (so + se).decode("utf-8", "replace").split("\n", 1) | ||||||
|  |         err = wfilter(err) or err | ||||||
|  |         return f"ERROR {rc}: {err[0]}" | ||||||
|  |  | ||||||
|  |     if se: | ||||||
|  |         err = se.decode("utf-8", "replace").split("\n", 1) | ||||||
|  |         err = wfilter(err) | ||||||
|  |         if err: | ||||||
|  |             return f"Warning: {err[0]}" | ||||||
|  |  | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = sys.argv[1] | ||||||
|  |     zb = sys.stdin.buffer.read() | ||||||
|  |     zs = zb.decode("utf-8", "replace") | ||||||
|  |     md = json.loads(zs) | ||||||
|  |  | ||||||
|  |     fdir = os.path.dirname(os.path.realpath(fp)) | ||||||
|  |     flag = os.path.join(fdir, ".processed") | ||||||
|  |     if os.path.exists(flag): | ||||||
|  |         return "already processed" | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         w, h = [int(x) for x in md["res"].split("x")] | ||||||
|  |         if not w + h: | ||||||
|  |             raise Exception() | ||||||
|  |     except: | ||||||
|  |         return "could not determine resolution" | ||||||
|  |  | ||||||
|  |     # grab streams/format metadata + 2 seconds of frames at the start and end | ||||||
|  |     zs = "ffprobe -hide_banner -v warning -of json -show_streams -show_format -show_packets -show_data_hash crc32 -read_intervals %+2,999999%+2" | ||||||
|  |     cmd = zs.encode("ascii").split(b" ") + [fsenc(fp)] | ||||||
|  |     p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) | ||||||
|  |     so, se = p.communicate() | ||||||
|  |  | ||||||
|  |     # spaces to tabs, drops filesize from 69k to 48k | ||||||
|  |     so = b"\n".join( | ||||||
|  |         [ | ||||||
|  |             b"\t" * int((len(x) - len(x.lstrip())) / 4) + x.lstrip() | ||||||
|  |             for x in (so or b"").split(b"\n") | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |     with open(fsenc(f"{fp}.ff.json"), "wb") as f: | ||||||
|  |         f.write(so) | ||||||
|  |  | ||||||
|  |     err = errchk(so, se, p.returncode, f"{fp}.vidchk") | ||||||
|  |     if err: | ||||||
|  |         return err | ||||||
|  |  | ||||||
|  |     if max(w, h) < 1280 and min(w, h) < 720: | ||||||
|  |         return "resolution too small" | ||||||
|  |  | ||||||
|  |     zs = ( | ||||||
|  |         "ffmpeg -y -hide_banner -nostdin -v warning" | ||||||
|  |         + " -err_detect +crccheck+bitstream+buffer+careful+compliant+aggressive+explode" | ||||||
|  |         + " -xerror -i" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     cmd = zs.encode("ascii").split(b" ") + [fsenc(fp)] | ||||||
|  |  | ||||||
|  |     if FAST: | ||||||
|  |         zs = "-c copy -f null -" | ||||||
|  |     else: | ||||||
|  |         zs = "-vcodec rawvideo -acodec pcm_s16le -f null -" | ||||||
|  |  | ||||||
|  |     cmd += zs.encode("ascii").split(b" ") | ||||||
|  |  | ||||||
|  |     p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) | ||||||
|  |     so, se = p.communicate() | ||||||
|  |     return errchk(so, se, p.returncode, f"{fp}.vidchk") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     print(main() or "ok") | ||||||
							
								
								
									
										94
									
								
								bin/mtag/wget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								bin/mtag/wget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | DEPRECATED -- replaced by event hooks; | ||||||
|  | https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/wget.py | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | use copyparty as a file downloader by POSTing URLs as | ||||||
|  | application/x-www-form-urlencoded (for example using the | ||||||
|  | message/pager function on the website) | ||||||
|  |  | ||||||
|  | example copyparty config to use this: | ||||||
|  |   --urlform save,get -vsrv/wget:wget:rwmd,ed:c,e2ts,mtp=title=ebin,t300,ad,bin/mtag/wget.py | ||||||
|  |  | ||||||
|  | explained: | ||||||
|  |   for realpath srv/wget (served at /wget) with read-write-modify-delete for ed, | ||||||
|  |   enable file analysis on upload (e2ts), | ||||||
|  |   use mtp plugin "bin/mtag/wget.py" to provide metadata tag "title", | ||||||
|  |   do this on all uploads with the file extension "bin", | ||||||
|  |   t300 = 300 seconds timeout for each dwonload, | ||||||
|  |   ad = parse file regardless if FFmpeg thinks it is audio or not | ||||||
|  |  | ||||||
|  | PS: this requires e2ts to be functional, | ||||||
|  |   meaning you need to do at least one of these: | ||||||
|  |    * apt install ffmpeg | ||||||
|  |    * pip3 install mutagen | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import subprocess as sp | ||||||
|  | from urllib.parse import unquote_to_bytes as unquote | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     fp = os.path.abspath(sys.argv[1]) | ||||||
|  |     fdir = os.path.dirname(fp) | ||||||
|  |     fname = os.path.basename(fp) | ||||||
|  |     if not fname.startswith("put-") or not fname.endswith(".bin"): | ||||||
|  |         raise Exception("not a post file") | ||||||
|  |  | ||||||
|  |     buf = b"" | ||||||
|  |     with open(fp, "rb") as f: | ||||||
|  |         while True: | ||||||
|  |             b = f.read(4096) | ||||||
|  |             buf += b | ||||||
|  |             if len(buf) > 4096: | ||||||
|  |                 raise Exception("too big") | ||||||
|  |  | ||||||
|  |             if not b: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |     if not buf: | ||||||
|  |         raise Exception("file is empty") | ||||||
|  |  | ||||||
|  |     buf = unquote(buf.replace(b"+", b" ")) | ||||||
|  |     url = buf.decode("utf-8") | ||||||
|  |  | ||||||
|  |     if not url.startswith("msg="): | ||||||
|  |         raise Exception("does not start with msg=") | ||||||
|  |  | ||||||
|  |     url = url[4:] | ||||||
|  |     if "://" not in url: | ||||||
|  |         url = "https://" + url | ||||||
|  |  | ||||||
|  |     proto = url.split("://")[0].lower() | ||||||
|  |     if proto not in ("http", "https", "ftp", "ftps"): | ||||||
|  |         raise Exception("bad proto {}".format(proto)) | ||||||
|  |  | ||||||
|  |     os.chdir(fdir) | ||||||
|  |  | ||||||
|  |     name = url.split("?")[0].split("/")[-1] | ||||||
|  |     tfn = "-- DOWNLOADING " + name | ||||||
|  |     open(tfn, "wb").close() | ||||||
|  |  | ||||||
|  |     cmd = ["wget", "--trust-server-names", "--", url] | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         sp.check_call(cmd) | ||||||
|  |  | ||||||
|  |         # OPTIONAL: | ||||||
|  |         #   on success, delete the .bin file which contains the URL | ||||||
|  |         os.unlink(fp) | ||||||
|  |     except: | ||||||
|  |         open("-- FAILED TO DONWLOAD " + name, "wb").close() | ||||||
|  |  | ||||||
|  |     os.unlink(tfn) | ||||||
|  |     print(url) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
| @@ -1,19 +1,26 @@ | |||||||
| #!/usr/bin/env python | #!/usr/bin/env python | ||||||
|  |  | ||||||
| import re | import re | ||||||
|  | import os | ||||||
| import sys | import sys | ||||||
| import gzip | import gzip | ||||||
| import json | import json | ||||||
|  | import base64 | ||||||
|  | import string | ||||||
|  | import urllib.request | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| """ | """ | ||||||
| youtube initial player response | youtube initial player response | ||||||
|  |  | ||||||
| example usage: | it's probably best to use this through a config file; see res/yt-ipr.conf | ||||||
|   -v srv/playerdata:playerdata:w |  | ||||||
|        :c,e2tsr:c,e2dsa | but if you want to use plain arguments instead then: | ||||||
|        :c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-expires=bin/mtag/yt-ipr.py |   -v srv/ytm:ytm:w:rw,ed | ||||||
|        :c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-expires |        :c,e2ts,e2dsa | ||||||
|  |        :c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H | ||||||
|  |        :c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py | ||||||
|  |        :c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires | ||||||
|  |  | ||||||
| see res/yt-ipr.user.js for the example userscript to go with this | see res/yt-ipr.user.js for the example userscript to go with this | ||||||
| """ | """ | ||||||
| @@ -30,19 +37,36 @@ def main(): | |||||||
|     txt = "{" + txt.split("{", 1)[1] |     txt = "{" + txt.split("{", 1)[1] | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         obj = json.loads(txt) |         pd = json.loads(txt) | ||||||
|     except json.decoder.JSONDecodeError as ex: |     except json.decoder.JSONDecodeError as ex: | ||||||
|         obj = json.loads(txt[: ex.pos]) |         pd = json.loads(txt[: ex.pos]) | ||||||
|  |  | ||||||
|     # print(json.dumps(obj, indent=2)) |     # print(json.dumps(pd, indent=2)) | ||||||
|  |  | ||||||
|     vd = obj["videoDetails"] |     if "videoDetails" in pd: | ||||||
|     sd = obj["streamingData"] |         parse_youtube(pd) | ||||||
|  |     else: | ||||||
|  |         parse_freg(pd) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_expiration(url): | ||||||
|  |     et = re.search(r"[?&]expire=([0-9]+)", url).group(1) | ||||||
|  |     et = datetime.utcfromtimestamp(int(et)) | ||||||
|  |     return et.strftime("%Y-%m-%d, %H:%M") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def parse_youtube(pd): | ||||||
|  |     vd = pd["videoDetails"] | ||||||
|  |     sd = pd["streamingData"] | ||||||
|  |  | ||||||
|     et = sd["adaptiveFormats"][0]["url"] |     et = sd["adaptiveFormats"][0]["url"] | ||||||
|     et = re.search(r"[?&]expire=([0-9]+)", et).group(1) |     et = get_expiration(et) | ||||||
|     et = datetime.utcfromtimestamp(int(et)) |  | ||||||
|     et = et.strftime("%Y-%m-%d, %H:%M") |     mf = [] | ||||||
|  |     if "dashManifestUrl" in sd: | ||||||
|  |         mf.append("dash") | ||||||
|  |     if "hlsManifestUrl" in sd: | ||||||
|  |         mf.append("hls") | ||||||
|  |  | ||||||
|     r = { |     r = { | ||||||
|         "yt-id": vd["videoId"], |         "yt-id": vd["videoId"], | ||||||
| @@ -52,10 +76,123 @@ def main(): | |||||||
|         "yt-views": vd["viewCount"], |         "yt-views": vd["viewCount"], | ||||||
|         "yt-private": vd["isPrivate"], |         "yt-private": vd["isPrivate"], | ||||||
|         # "yt-expires": sd["expiresInSeconds"], |         # "yt-expires": sd["expiresInSeconds"], | ||||||
|  |         "yt-manifest": ",".join(mf), | ||||||
|         "yt-expires": et, |         "yt-expires": et, | ||||||
|     } |     } | ||||||
|     print(json.dumps(r)) |     print(json.dumps(r)) | ||||||
|  |  | ||||||
|  |     freg_conv(pd) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def parse_freg(pd): | ||||||
|  |     md = pd["metadata"] | ||||||
|  |     r = { | ||||||
|  |         "yt-id": md["id"], | ||||||
|  |         "yt-title": md["title"], | ||||||
|  |         "yt-author": md["channelName"], | ||||||
|  |         "yt-channel": md["channelURL"].strip("/").split("/")[-1], | ||||||
|  |         "yt-expires": get_expiration(list(pd["video"].values())[0]), | ||||||
|  |     } | ||||||
|  |     print(json.dumps(r)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def freg_conv(pd): | ||||||
|  |     # based on getURLs.js v1.5 (2021-08-07) | ||||||
|  |     # fmt: off | ||||||
|  |     priority = { | ||||||
|  |         "video": [ | ||||||
|  |             337, 315, 266, 138,  # 2160p60 | ||||||
|  |             313, 336,  # 2160p | ||||||
|  |             308,  # 1440p60 | ||||||
|  |             271, 264,  # 1440p | ||||||
|  |             335, 303, 299,  # 1080p60 | ||||||
|  |             248, 169, 137,  # 1080p | ||||||
|  |             334, 302, 298,  # 720p60 | ||||||
|  |             247, 136  # 720p | ||||||
|  |         ], | ||||||
|  |         "audio": [ | ||||||
|  |             251, 141, 171, 140, 250, 249, 139 | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     vid_id = pd["videoDetails"]["videoId"] | ||||||
|  |     chan_id = pd["videoDetails"]["channelId"] | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"] | ||||||
|  |         start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"] | ||||||
|  |     except: | ||||||
|  |         thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg" | ||||||
|  |         start_ts = "" | ||||||
|  |  | ||||||
|  |     # fmt: on | ||||||
|  |  | ||||||
|  |     metadata = { | ||||||
|  |         "title": pd["videoDetails"]["title"], | ||||||
|  |         "id": vid_id, | ||||||
|  |         "channelName": pd["videoDetails"]["author"], | ||||||
|  |         "channelURL": "https://www.youtube.com/channel/" + chan_id, | ||||||
|  |         "description": pd["videoDetails"]["shortDescription"], | ||||||
|  |         "thumbnailUrl": thumb_url, | ||||||
|  |         "startTimestamp": start_ts, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]: | ||||||
|  |         print(f"malicious json", file=sys.stderr) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     basepath = os.path.dirname(sys.argv[1]) | ||||||
|  |  | ||||||
|  |     thumb_fn = f"{basepath}/{vid_id}.jpg" | ||||||
|  |     tmp_fn = f"{thumb_fn}.{os.getpid()}" | ||||||
|  |     if not os.path.exists(thumb_fn) and ( | ||||||
|  |         thumb_url.startswith("https://img.youtube.com/vi/") | ||||||
|  |         or thumb_url.startswith("https://i.ytimg.com/vi/") | ||||||
|  |     ): | ||||||
|  |         try: | ||||||
|  |             with urllib.request.urlopen(thumb_url) as fi: | ||||||
|  |                 with open(tmp_fn, "wb") as fo: | ||||||
|  |                     fo.write(fi.read()) | ||||||
|  |  | ||||||
|  |             os.rename(tmp_fn, thumb_fn) | ||||||
|  |         except: | ||||||
|  |             if os.path.exists(tmp_fn): | ||||||
|  |                 os.unlink(tmp_fn) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         with open(thumb_fn, "rb") as f: | ||||||
|  |             thumb = base64.b64encode(f.read()).decode("ascii") | ||||||
|  |     except: | ||||||
|  |         thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=" | ||||||
|  |  | ||||||
|  |     metadata["thumbnail"] = "data:image/jpeg;base64," + thumb | ||||||
|  |  | ||||||
|  |     ret = { | ||||||
|  |         "metadata": metadata, | ||||||
|  |         "version": "1.5", | ||||||
|  |         "createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for stream, itags in priority.items(): | ||||||
|  |         for itag in itags: | ||||||
|  |             url = None | ||||||
|  |             for afmt in pd["streamingData"]["adaptiveFormats"]: | ||||||
|  |                 if itag == afmt["itag"]: | ||||||
|  |                     url = afmt["url"] | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |             if url: | ||||||
|  |                 ret[stream] = {itag: url} | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |     fn = f"{basepath}/{vid_id}.urls.json" | ||||||
|  |     with open(fn, "w", encoding="utf-8", errors="replace") as f: | ||||||
|  |         f.write(json.dumps(ret, indent=4)) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     main() |     try: | ||||||
|  |         main() | ||||||
|  |     except: | ||||||
|  |         # raise | ||||||
|  |         pass | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
| 
 | 
 | ||||||
| """copyparty-fuse-streaming: remote copyparty as a local filesystem""" | """partyfuse-streaming: remote copyparty as a local filesystem""" | ||||||
| __author__ = "ed <copyparty@ocv.me>" | __author__ = "ed <copyparty@ocv.me>" | ||||||
| __copyright__ = 2020 | __copyright__ = 2020 | ||||||
| __license__ = "MIT" | __license__ = "MIT" | ||||||
| @@ -12,7 +12,7 @@ __url__ = "https://github.com/9001/copyparty/" | |||||||
| mount a copyparty server (local or remote) as a filesystem | mount a copyparty server (local or remote) as a filesystem | ||||||
| 
 | 
 | ||||||
| usage: | usage: | ||||||
|   python copyparty-fuse-streaming.py http://192.168.1.69:3923/  ./music |   python partyfuse-streaming.py http://192.168.1.69:3923/  ./music | ||||||
| 
 | 
 | ||||||
| dependencies: | dependencies: | ||||||
|   python3 -m pip install --user fusepy |   python3 -m pip install --user fusepy | ||||||
| @@ -21,7 +21,7 @@ dependencies: | |||||||
|   + on Windows: https://github.com/billziss-gh/winfsp/releases/latest |   + on Windows: https://github.com/billziss-gh/winfsp/releases/latest | ||||||
| 
 | 
 | ||||||
| this was a mistake: | this was a mistake: | ||||||
|   fork of copyparty-fuse.py with a streaming cache rather than readahead, |   fork of partyfuse.py with a streaming cache rather than readahead, | ||||||
|   thought this was gonna be way faster (and it kind of is) |   thought this was gonna be way faster (and it kind of is) | ||||||
|   except the overhead of reopening connections on trunc totally kills it |   except the overhead of reopening connections on trunc totally kills it | ||||||
| """ | """ | ||||||
| @@ -42,6 +42,7 @@ import threading | |||||||
| import traceback | import traceback | ||||||
| import http.client  # py2: httplib | import http.client  # py2: httplib | ||||||
| import urllib.parse | import urllib.parse | ||||||
|  | import calendar | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from urllib.parse import quote_from_bytes as quote | from urllib.parse import quote_from_bytes as quote | ||||||
| from urllib.parse import unquote_to_bytes as unquote | from urllib.parse import unquote_to_bytes as unquote | ||||||
| @@ -61,12 +62,12 @@ except: | |||||||
|     else: |     else: | ||||||
|         libfuse = "apt install libfuse\n    modprobe fuse" |         libfuse = "apt install libfuse\n    modprobe fuse" | ||||||
| 
 | 
 | ||||||
|     print( |     m = """\033[33m | ||||||
|         "\n  could not import fuse; these may help:" |   could not import fuse; these may help: | ||||||
|         + "\n    python3 -m pip install --user fusepy\n    " |     {} -m pip install --user fusepy | ||||||
|         + libfuse |     {} | ||||||
|         + "\n" | \033[0m""" | ||||||
|     ) |     print(m.format(sys.executable, libfuse)) | ||||||
|     raise |     raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -153,7 +154,7 @@ def dewin(txt): | |||||||
| class RecentLog(object): | class RecentLog(object): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.mtx = threading.Lock() |         self.mtx = threading.Lock() | ||||||
|         self.f = None  # open("copyparty-fuse.log", "wb") |         self.f = None  # open("partyfuse.log", "wb") | ||||||
|         self.q = [] |         self.q = [] | ||||||
| 
 | 
 | ||||||
|         thr = threading.Thread(target=self.printer) |         thr = threading.Thread(target=self.printer) | ||||||
| @@ -184,9 +185,9 @@ class RecentLog(object): | |||||||
|             print("".join(q), end="") |             print("".join(q), end="") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # [windows/cmd/cpy3]  python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/cmd/cpy3]  python dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # | # | ||||||
| # [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done | # [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done | ||||||
| # [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done | # [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done | ||||||
| @@ -495,7 +496,7 @@ class Gateway(object): | |||||||
|                 ts = 60 * 60 * 24 * 2 |                 ts = 60 * 60 * 24 * 2 | ||||||
|                 try: |                 try: | ||||||
|                     sz = int(fsize) |                     sz = int(fsize) | ||||||
|                     ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp() |                     ts = calendar.timegm(time.strptime(fdate, "%Y-%m-%d %H:%M:%S")) | ||||||
|                 except: |                 except: | ||||||
|                     info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) |                     info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) | ||||||
|                     # python cannot strptime(1959-01-01) on windows |                     # python cannot strptime(1959-01-01) on windows | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
| 
 | 
 | ||||||
| """copyparty-fuse: remote copyparty as a local filesystem""" | """partyfuse: remote copyparty as a local filesystem""" | ||||||
| __author__ = "ed <copyparty@ocv.me>" | __author__ = "ed <copyparty@ocv.me>" | ||||||
| __copyright__ = 2019 | __copyright__ = 2019 | ||||||
| __license__ = "MIT" | __license__ = "MIT" | ||||||
| @@ -12,7 +12,7 @@ __url__ = "https://github.com/9001/copyparty/" | |||||||
| mount a copyparty server (local or remote) as a filesystem | mount a copyparty server (local or remote) as a filesystem | ||||||
| 
 | 
 | ||||||
| usage: | usage: | ||||||
|   python copyparty-fuse.py http://192.168.1.69:3923/  ./music |   python partyfuse.py http://192.168.1.69:3923/  ./music | ||||||
| 
 | 
 | ||||||
| dependencies: | dependencies: | ||||||
|   python3 -m pip install --user fusepy |   python3 -m pip install --user fusepy | ||||||
| @@ -22,7 +22,7 @@ dependencies: | |||||||
| 
 | 
 | ||||||
| note: | note: | ||||||
|   you probably want to run this on windows clients: |   you probably want to run this on windows clients: | ||||||
|   https://github.com/9001/copyparty/blob/master/contrib/explorer-nothumbs-nofoldertypes.reg |   https://github.com/9001/copyparty/blob/hovudstraum/contrib/explorer-nothumbs-nofoldertypes.reg | ||||||
| 
 | 
 | ||||||
| get server cert: | get server cert: | ||||||
|   awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem |   awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem | ||||||
| @@ -45,6 +45,7 @@ import threading | |||||||
| import traceback | import traceback | ||||||
| import http.client  # py2: httplib | import http.client  # py2: httplib | ||||||
| import urllib.parse | import urllib.parse | ||||||
|  | import calendar | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from urllib.parse import quote_from_bytes as quote | from urllib.parse import quote_from_bytes as quote | ||||||
| from urllib.parse import unquote_to_bytes as unquote | from urllib.parse import unquote_to_bytes as unquote | ||||||
| @@ -71,14 +72,14 @@ except: | |||||||
|     elif MACOS: |     elif MACOS: | ||||||
|         libfuse = "install https://osxfuse.github.io/" |         libfuse = "install https://osxfuse.github.io/" | ||||||
|     else: |     else: | ||||||
|         libfuse = "apt install libfuse\n    modprobe fuse" |         libfuse = "apt install libfuse3-3\n    modprobe fuse" | ||||||
| 
 | 
 | ||||||
|     print( |     m = """\033[33m | ||||||
|         "\n  could not import fuse; these may help:" |   could not import fuse; these may help: | ||||||
|         + "\n    python3 -m pip install --user fusepy\n    " |     {} -m pip install --user fusepy | ||||||
|         + libfuse |     {} | ||||||
|         + "\n" | \033[0m""" | ||||||
|     ) |     print(m.format(sys.executable, libfuse)) | ||||||
|     raise |     raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -165,7 +166,7 @@ def dewin(txt): | |||||||
| class RecentLog(object): | class RecentLog(object): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.mtx = threading.Lock() |         self.mtx = threading.Lock() | ||||||
|         self.f = None  # open("copyparty-fuse.log", "wb") |         self.f = None  # open("partyfuse.log", "wb") | ||||||
|         self.q = [] |         self.q = [] | ||||||
| 
 | 
 | ||||||
|         thr = threading.Thread(target=self.printer) |         thr = threading.Thread(target=self.printer) | ||||||
| @@ -196,9 +197,9 @@ class RecentLog(object): | |||||||
|             print("".join(q), end="") |             print("".join(q), end="") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # [windows/cmd/cpy3]  python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/cmd/cpy3]  python dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/ | # [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/partyfuse.py q: http://192.168.1.159:1234/ | ||||||
| # | # | ||||||
| # [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done | # [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done | ||||||
| # [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done | # [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done | ||||||
| @@ -393,15 +394,16 @@ class Gateway(object): | |||||||
| 
 | 
 | ||||||
|         rsp = json.loads(rsp.decode("utf-8")) |         rsp = json.loads(rsp.decode("utf-8")) | ||||||
|         ret = [] |         ret = [] | ||||||
|         for is_dir, nodes in [[True, rsp["dirs"]], [False, rsp["files"]]]: |         for statfun, nodes in [ | ||||||
|  |             [self.stat_dir, rsp["dirs"]], | ||||||
|  |             [self.stat_file, rsp["files"]], | ||||||
|  |         ]: | ||||||
|             for n in nodes: |             for n in nodes: | ||||||
|                 fname = unquote(n["href"]).rstrip(b"/") |                 fname = unquote(n["href"].split("?")[0]).rstrip(b"/").decode("wtf-8") | ||||||
|                 fname = fname.decode("wtf-8") |  | ||||||
|                 if bad_good: |                 if bad_good: | ||||||
|                     fname = enwin(fname) |                     fname = enwin(fname) | ||||||
| 
 | 
 | ||||||
|                 fun = self.stat_dir if is_dir else self.stat_file |                 ret.append([fname, statfun(n["ts"], n["sz"]), 0]) | ||||||
|                 ret.append([fname, fun(n["ts"], n["sz"]), 0]) |  | ||||||
| 
 | 
 | ||||||
|         return ret |         return ret | ||||||
| 
 | 
 | ||||||
| @@ -442,7 +444,7 @@ class Gateway(object): | |||||||
|                 ts = 60 * 60 * 24 * 2 |                 ts = 60 * 60 * 24 * 2 | ||||||
|                 try: |                 try: | ||||||
|                     sz = int(fsize) |                     sz = int(fsize) | ||||||
|                     ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp() |                     ts = calendar.timegm(time.strptime(fdate, "%Y-%m-%d %H:%M:%S")) | ||||||
|                 except: |                 except: | ||||||
|                     info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) |                     info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) | ||||||
|                     # python cannot strptime(1959-01-01) on windows |                     # python cannot strptime(1959-01-01) on windows | ||||||
| @@ -995,7 +997,7 @@ def main(): | |||||||
|     ap.add_argument( |     ap.add_argument( | ||||||
|         "-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache" |         "-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache" | ||||||
|     ) |     ) | ||||||
|     ap.add_argument("-a", metavar="PASSWORD", help="password") |     ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath") | ||||||
|     ap.add_argument("-d", action="store_true", help="enable debug") |     ap.add_argument("-d", action="store_true", help="enable debug") | ||||||
|     ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify") |     ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify") | ||||||
|     ap.add_argument("-td", action="store_true", help="disable certificate check") |     ap.add_argument("-td", action="store_true", help="disable certificate check") | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
| 
 | 
 | ||||||
| """copyparty-fuseb: remote copyparty as a local filesystem""" | """partyfuse2: remote copyparty as a local filesystem""" | ||||||
| __author__ = "ed <copyparty@ocv.me>" | __author__ = "ed <copyparty@ocv.me>" | ||||||
| __copyright__ = 2020 | __copyright__ = 2020 | ||||||
| __license__ = "MIT" | __license__ = "MIT" | ||||||
| @@ -11,14 +11,18 @@ import re | |||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
| import time | import time | ||||||
|  | import json | ||||||
| import stat | import stat | ||||||
| import errno | import errno | ||||||
| import struct | import struct | ||||||
|  | import codecs | ||||||
|  | import platform | ||||||
| import threading | import threading | ||||||
| import http.client  # py2: httplib | import http.client  # py2: httplib | ||||||
| import urllib.parse | import urllib.parse | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from urllib.parse import quote_from_bytes as quote | from urllib.parse import quote_from_bytes as quote | ||||||
|  | from urllib.parse import unquote_to_bytes as unquote | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|     import fuse |     import fuse | ||||||
| @@ -28,9 +32,19 @@ try: | |||||||
|     if not hasattr(fuse, "__version__"): |     if not hasattr(fuse, "__version__"): | ||||||
|         raise Exception("your fuse-python is way old") |         raise Exception("your fuse-python is way old") | ||||||
| except: | except: | ||||||
|     print( |     if WINDOWS: | ||||||
|         "\n  could not import fuse; these may help:\n    python3 -m pip install --user fuse-python\n    apt install libfuse\n    modprobe fuse\n" |         libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest" | ||||||
|     ) |     elif MACOS: | ||||||
|  |         libfuse = "install https://osxfuse.github.io/" | ||||||
|  |     else: | ||||||
|  |         libfuse = "apt install libfuse\n    modprobe fuse" | ||||||
|  | 
 | ||||||
|  |     m = """\033[33m | ||||||
|  |   could not import fuse; these may help: | ||||||
|  |     {} -m pip install --user fuse-python | ||||||
|  |     {} | ||||||
|  | \033[0m""" | ||||||
|  |     print(m.format(sys.executable, libfuse)) | ||||||
|     raise |     raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -38,18 +52,22 @@ except: | |||||||
| mount a copyparty server (local or remote) as a filesystem | mount a copyparty server (local or remote) as a filesystem | ||||||
| 
 | 
 | ||||||
| usage: | usage: | ||||||
|   python ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas |   python ./partyfuse2.py -f -o allow_other,auto_unmount,nonempty,pw=wark,url=http://192.168.1.69:3923 /mnt/nas | ||||||
| 
 | 
 | ||||||
| dependencies: | dependencies: | ||||||
|   sudo apk add fuse-dev python3-dev |   sudo apk add fuse-dev python3-dev | ||||||
|   python3 -m pip install --user fuse-python |   python3 -m pip install --user fuse-python | ||||||
| 
 | 
 | ||||||
| fork of copyparty-fuse.py based on fuse-python which | fork of partyfuse.py based on fuse-python which | ||||||
|   appears to be more compliant than fusepy? since this works with samba |   appears to be more compliant than fusepy? since this works with samba | ||||||
|     (probably just my garbage code tbh) |     (probably just my garbage code tbh) | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | WINDOWS = sys.platform == "win32" | ||||||
|  | MACOS = platform.system() == "Darwin" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def threadless_log(msg): | def threadless_log(msg): | ||||||
|     print(msg + "\n", end="") |     print(msg + "\n", end="") | ||||||
| 
 | 
 | ||||||
| @@ -93,6 +111,41 @@ def html_dec(txt): | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def register_wtf8(): | ||||||
|  |     def wtf8_enc(text): | ||||||
|  |         return str(text).encode("utf-8", "surrogateescape"), len(text) | ||||||
|  | 
 | ||||||
|  |     def wtf8_dec(binary): | ||||||
|  |         return bytes(binary).decode("utf-8", "surrogateescape"), len(binary) | ||||||
|  | 
 | ||||||
|  |     def wtf8_search(encoding_name): | ||||||
|  |         return codecs.CodecInfo(wtf8_enc, wtf8_dec, name="wtf-8") | ||||||
|  | 
 | ||||||
|  |     codecs.register(wtf8_search) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | bad_good = {} | ||||||
|  | good_bad = {} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def enwin(txt): | ||||||
|  |     return "".join([bad_good.get(x, x) for x in txt]) | ||||||
|  | 
 | ||||||
|  |     for bad, good in bad_good.items(): | ||||||
|  |         txt = txt.replace(bad, good) | ||||||
|  | 
 | ||||||
|  |     return txt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def dewin(txt): | ||||||
|  |     return "".join([good_bad.get(x, x) for x in txt]) | ||||||
|  | 
 | ||||||
|  |     for bad, good in bad_good.items(): | ||||||
|  |         txt = txt.replace(good, bad) | ||||||
|  | 
 | ||||||
|  |     return txt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class CacheNode(object): | class CacheNode(object): | ||||||
|     def __init__(self, tag, data): |     def __init__(self, tag, data): | ||||||
|         self.tag = tag |         self.tag = tag | ||||||
| @@ -115,8 +168,9 @@ class Stat(fuse.Stat): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Gateway(object): | class Gateway(object): | ||||||
|     def __init__(self, base_url): |     def __init__(self, base_url, pw): | ||||||
|         self.base_url = base_url |         self.base_url = base_url | ||||||
|  |         self.pw = pw | ||||||
| 
 | 
 | ||||||
|         ui = urllib.parse.urlparse(base_url) |         ui = urllib.parse.urlparse(base_url) | ||||||
|         self.web_root = ui.path.strip("/") |         self.web_root = ui.path.strip("/") | ||||||
| @@ -135,8 +189,7 @@ class Gateway(object): | |||||||
|         self.conns = {} |         self.conns = {} | ||||||
| 
 | 
 | ||||||
|     def quotep(self, path): |     def quotep(self, path): | ||||||
|         # TODO: mojibake support |         path = path.encode("wtf-8") | ||||||
|         path = path.encode("utf-8", "ignore") |  | ||||||
|         return quote(path, safe="/") |         return quote(path, safe="/") | ||||||
| 
 | 
 | ||||||
|     def getconn(self, tid=None): |     def getconn(self, tid=None): | ||||||
| @@ -159,20 +212,29 @@ class Gateway(object): | |||||||
|         except: |         except: | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     def sendreq(self, *args, **kwargs): |     def sendreq(self, *args, **ka): | ||||||
|         tid = get_tid() |         tid = get_tid() | ||||||
|  |         if self.pw: | ||||||
|  |             ck = "cppwd=" + self.pw | ||||||
|  |             try: | ||||||
|  |                 ka["headers"]["Cookie"] = ck | ||||||
|  |             except: | ||||||
|  |                 ka["headers"] = {"Cookie": ck} | ||||||
|         try: |         try: | ||||||
|             c = self.getconn(tid) |             c = self.getconn(tid) | ||||||
|             c.request(*list(args), **kwargs) |             c.request(*list(args), **ka) | ||||||
|             return c.getresponse() |             return c.getresponse() | ||||||
|         except: |         except: | ||||||
|             self.closeconn(tid) |             self.closeconn(tid) | ||||||
|             c = self.getconn(tid) |             c = self.getconn(tid) | ||||||
|             c.request(*list(args), **kwargs) |             c.request(*list(args), **ka) | ||||||
|             return c.getresponse() |             return c.getresponse() | ||||||
| 
 | 
 | ||||||
|     def listdir(self, path): |     def listdir(self, path): | ||||||
|         web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots" |         if bad_good: | ||||||
|  |             path = dewin(path) | ||||||
|  | 
 | ||||||
|  |         web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls" | ||||||
|         r = self.sendreq("GET", web_path) |         r = self.sendreq("GET", web_path) | ||||||
|         if r.status != 200: |         if r.status != 200: | ||||||
|             self.closeconn() |             self.closeconn() | ||||||
| @@ -182,9 +244,12 @@ class Gateway(object): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         return self.parse_html(r) |         return self.parse_jls(r) | ||||||
| 
 | 
 | ||||||
|     def download_file_range(self, path, ofs1, ofs2): |     def download_file_range(self, path, ofs1, ofs2): | ||||||
|  |         if bad_good: | ||||||
|  |             path = dewin(path) | ||||||
|  | 
 | ||||||
|         web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw" |         web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw" | ||||||
|         hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1) |         hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1) | ||||||
|         log("downloading {}".format(hdr_range)) |         log("downloading {}".format(hdr_range)) | ||||||
| @@ -200,40 +265,27 @@ class Gateway(object): | |||||||
| 
 | 
 | ||||||
|         return r.read() |         return r.read() | ||||||
| 
 | 
 | ||||||
|     def parse_html(self, datasrc): |     def parse_jls(self, datasrc): | ||||||
|         ret = [] |         rsp = b"" | ||||||
|         remainder = b"" |  | ||||||
|         ptn = re.compile( |  | ||||||
|             r"^<tr><td>(-|DIR)</td><td><a [^>]+>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$" |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         while True: |         while True: | ||||||
|             buf = remainder + datasrc.read(4096) |             buf = datasrc.read(1024 * 32) | ||||||
|             # print('[{}]'.format(buf.decode('utf-8'))) |  | ||||||
|             if not buf: |             if not buf: | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
|             remainder = b"" |             rsp += buf | ||||||
|             endpos = buf.rfind(b"\n") |  | ||||||
|             if endpos >= 0: |  | ||||||
|                 remainder = buf[endpos + 1 :] |  | ||||||
|                 buf = buf[:endpos] |  | ||||||
| 
 | 
 | ||||||
|             lines = buf.decode("utf-8").split("\n") |         rsp = json.loads(rsp.decode("utf-8")) | ||||||
|             for line in lines: |         ret = [] | ||||||
|                 m = ptn.match(line) |         for statfun, nodes in [ | ||||||
|                 if not m: |             [self.stat_dir, rsp["dirs"]], | ||||||
|                     # print(line) |             [self.stat_file, rsp["files"]], | ||||||
|                     continue |         ]: | ||||||
|  |             for n in nodes: | ||||||
|  |                 fname = unquote(n["href"].split("?")[0]).rstrip(b"/").decode("wtf-8") | ||||||
|  |                 if bad_good: | ||||||
|  |                     fname = enwin(fname) | ||||||
| 
 | 
 | ||||||
|                 ftype, fname, fsize, fdate = m.groups() |                 ret.append([fname, statfun(n["ts"], n["sz"]), 0]) | ||||||
|                 fname = html_dec(fname) |  | ||||||
|                 ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp() |  | ||||||
|                 sz = int(fsize) |  | ||||||
|                 if ftype == "-": |  | ||||||
|                     ret.append([fname, self.stat_file(ts, sz), 0]) |  | ||||||
|                 else: |  | ||||||
|                     ret.append([fname, self.stat_dir(ts, sz), 0]) |  | ||||||
| 
 | 
 | ||||||
|         return ret |         return ret | ||||||
| 
 | 
 | ||||||
| @@ -262,6 +314,7 @@ class CPPF(Fuse): | |||||||
|         Fuse.__init__(self, *args, **kwargs) |         Fuse.__init__(self, *args, **kwargs) | ||||||
| 
 | 
 | ||||||
|         self.url = None |         self.url = None | ||||||
|  |         self.pw = None | ||||||
| 
 | 
 | ||||||
|         self.dircache = [] |         self.dircache = [] | ||||||
|         self.dircache_mtx = threading.Lock() |         self.dircache_mtx = threading.Lock() | ||||||
| @@ -271,7 +324,7 @@ class CPPF(Fuse): | |||||||
| 
 | 
 | ||||||
|     def init2(self): |     def init2(self): | ||||||
|         # TODO figure out how python-fuse wanted this to go |         # TODO figure out how python-fuse wanted this to go | ||||||
|         self.gw = Gateway(self.url)  # .decode('utf-8')) |         self.gw = Gateway(self.url, self.pw)  # .decode('utf-8')) | ||||||
|         info("up") |         info("up") | ||||||
| 
 | 
 | ||||||
|     def clean_dircache(self): |     def clean_dircache(self): | ||||||
| @@ -536,6 +589,8 @@ class CPPF(Fuse): | |||||||
| 
 | 
 | ||||||
|     def getattr(self, path): |     def getattr(self, path): | ||||||
|         log("getattr [{}]".format(path)) |         log("getattr [{}]".format(path)) | ||||||
|  |         if WINDOWS: | ||||||
|  |             path = enwin(path)  # windows occasionally decodes f0xx to xx | ||||||
| 
 | 
 | ||||||
|         path = path.strip("/") |         path = path.strip("/") | ||||||
|         try: |         try: | ||||||
| @@ -568,9 +623,25 @@ class CPPF(Fuse): | |||||||
| 
 | 
 | ||||||
| def main(): | def main(): | ||||||
|     time.strptime("19970815", "%Y%m%d")  # python#7980 |     time.strptime("19970815", "%Y%m%d")  # python#7980 | ||||||
|  |     register_wtf8() | ||||||
|  |     if WINDOWS: | ||||||
|  |         os.system("rem") | ||||||
|  | 
 | ||||||
|  |         for ch in '<>:"\\|?*': | ||||||
|  |             # microsoft maps illegal characters to f0xx | ||||||
|  |             # (e000 to f8ff is basic-plane private-use) | ||||||
|  |             bad_good[ch] = chr(ord(ch) + 0xF000) | ||||||
|  | 
 | ||||||
|  |         for n in range(0, 0x100): | ||||||
|  |             # map surrogateescape to another private-use area | ||||||
|  |             bad_good[chr(n + 0xDC00)] = chr(n + 0xF100) | ||||||
|  | 
 | ||||||
|  |         for k, v in bad_good.items(): | ||||||
|  |             good_bad[v] = k | ||||||
| 
 | 
 | ||||||
|     server = CPPF() |     server = CPPF() | ||||||
|     server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None) |     server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None) | ||||||
|  |     server.parser.add_option(mountopt="pw", metavar="PASSWORD", default=None) | ||||||
|     server.parse(values=server, errex=1) |     server.parse(values=server, errex=1) | ||||||
|     if not server.url or not str(server.url).startswith("http"): |     if not server.url or not str(server.url).startswith("http"): | ||||||
|         print("\nerror:") |         print("\nerror:") | ||||||
| @@ -578,7 +649,7 @@ def main(): | |||||||
|         print("  need argument: mount-path") |         print("  need argument: mount-path") | ||||||
|         print("example:") |         print("example:") | ||||||
|         print( |         print( | ||||||
|             "  ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas" |             "  ./partyfuse2.py -f -o allow_other,auto_unmount,nonempty,pw=wark,url=http://192.168.1.69:3923 /mnt/nas" | ||||||
|         ) |         ) | ||||||
|         sys.exit(1) |         sys.exit(1) | ||||||
| 
 | 
 | ||||||
							
								
								
									
										177
									
								
								bin/partyjournal.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										177
									
								
								bin/partyjournal.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | partyjournal.py: chronological history of uploads | ||||||
|  | 2021-12-31, v0.1, ed <irc.rizon.net>, MIT-Licensed | ||||||
|  | https://github.com/9001/copyparty/blob/hovudstraum/bin/partyjournal.py | ||||||
|  |  | ||||||
|  | produces a chronological list of all uploads, | ||||||
|  | by collecting info from up2k databases and the filesystem | ||||||
|  |  | ||||||
|  | specify subnet `192.168.1.*` with argument `.=192.168.1.`, | ||||||
|  | affecting all successive mappings | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   ./partyjournal.py > partyjournal.html .=192.168.1. cart=125 steen=114 steen=131 sleepy=121 fscarlet=144 ed=101 ed=123 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  | import base64 | ||||||
|  | import sqlite3 | ||||||
|  | import argparse | ||||||
|  | from datetime import datetime | ||||||
|  | from urllib.parse import quote_from_bytes as quote | ||||||
|  | from urllib.parse import unquote_to_bytes as unquote | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FS_ENCODING = sys.getfilesystemencoding() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## | ||||||
|  | ## snibbed from copyparty | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def s3dec(v): | ||||||
|  |     if not v.startswith("//"): | ||||||
|  |         return v | ||||||
|  |  | ||||||
|  |     v = base64.urlsafe_b64decode(v.encode("ascii")[2:]) | ||||||
|  |     return v.decode(FS_ENCODING, "replace") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def quotep(txt): | ||||||
|  |     btxt = txt.encode("utf-8", "replace") | ||||||
|  |     quot1 = quote(btxt, safe=b"/") | ||||||
|  |     quot1 = quot1.encode("ascii") | ||||||
|  |     quot2 = quot1.replace(b" ", b"+") | ||||||
|  |     return quot2.decode("utf-8", "replace") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def html_escape(s, quote=False, crlf=False): | ||||||
|  |     """html.escape but also newlines""" | ||||||
|  |     s = s.replace("&", "&").replace("<", "<").replace(">", ">") | ||||||
|  |     if quote: | ||||||
|  |         s = s.replace('"', """).replace("'", "'") | ||||||
|  |     if crlf: | ||||||
|  |         s = s.replace("\r", "
").replace("\n", "
") | ||||||
|  |  | ||||||
|  |     return s | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## end snibs | ||||||
|  | ## | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     ap = argparse.ArgumentParser(formatter_class=APF) | ||||||
|  |     ap.add_argument("who", nargs="*") | ||||||
|  |     ar = ap.parse_args() | ||||||
|  |  | ||||||
|  |     imap = {} | ||||||
|  |     subnet = "" | ||||||
|  |     for v in ar.who: | ||||||
|  |         if "=" not in v: | ||||||
|  |             raise Exception("bad who: " + v) | ||||||
|  |  | ||||||
|  |         k, v = v.split("=") | ||||||
|  |         if k == ".": | ||||||
|  |             subnet = v | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         imap["{}{}".format(subnet, v)] = k | ||||||
|  |  | ||||||
|  |     print(repr(imap), file=sys.stderr) | ||||||
|  |  | ||||||
|  |     print( | ||||||
|  |         """\ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head><meta charset="utf-8"><style> | ||||||
|  |  | ||||||
|  | html, body { | ||||||
|  |     color: #ccc; | ||||||
|  |     background: #222; | ||||||
|  |     font-family: sans-serif; | ||||||
|  | } | ||||||
|  | a { | ||||||
|  |     color: #fc5; | ||||||
|  | } | ||||||
|  | td, th { | ||||||
|  |     padding: .2em .5em; | ||||||
|  |     border: 1px solid #999; | ||||||
|  |     border-width: 0 1px 1px 0; | ||||||
|  |     white-space: nowrap; | ||||||
|  | } | ||||||
|  | td:nth-child(1), | ||||||
|  | td:nth-child(2), | ||||||
|  | td:nth-child(3) { | ||||||
|  |     font-family: monospace, monospace; | ||||||
|  |     text-align: right; | ||||||
|  | } | ||||||
|  | tr:first-child { | ||||||
|  |     position: sticky; | ||||||
|  |     top: -1px; | ||||||
|  | } | ||||||
|  | th { | ||||||
|  |     background: #222; | ||||||
|  |     text-align: left; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </style></head><body><table><tr> | ||||||
|  |     <th>wark</th> | ||||||
|  |     <th>time</th> | ||||||
|  |     <th>size</th> | ||||||
|  |     <th>who</th> | ||||||
|  |     <th>link</th> | ||||||
|  | </tr>""" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     db_path = ".hist/up2k.db" | ||||||
|  |     conn = sqlite3.connect(db_path) | ||||||
|  |     q = r"pragma table_info(up)" | ||||||
|  |     inf = conn.execute(q).fetchall() | ||||||
|  |     cols = [x[1] for x in inf] | ||||||
|  |     print("<!-- " + str(cols) + " -->") | ||||||
|  |     # ['w', 'mt', 'sz', 'rd', 'fn', 'ip', 'at'] | ||||||
|  |  | ||||||
|  |     q = r"select * from up order by case when at > 0 then at else mt end" | ||||||
|  |     for w, mt, sz, rd, fn, ip, at in conn.execute(q): | ||||||
|  |         link = "/".join([s3dec(x) for x in [rd, fn] if x]) | ||||||
|  |         if fn.startswith("put-") and sz < 4096: | ||||||
|  |             try: | ||||||
|  |                 with open(link, "rb") as f: | ||||||
|  |                     txt = f.read().decode("utf-8", "replace") | ||||||
|  |             except: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             if txt.startswith("msg="): | ||||||
|  |                 txt = txt.encode("utf-8", "replace") | ||||||
|  |                 txt = unquote(txt.replace(b"+", b" ")) | ||||||
|  |                 link = txt.decode("utf-8")[4:] | ||||||
|  |  | ||||||
|  |         sz = "{:,}".format(sz) | ||||||
|  |         v = [ | ||||||
|  |             w[:16], | ||||||
|  |             datetime.utcfromtimestamp(at if at > 0 else mt).strftime( | ||||||
|  |                 "%Y-%m-%d %H:%M:%S" | ||||||
|  |             ), | ||||||
|  |             sz, | ||||||
|  |             imap.get(ip, ip), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         row = "<tr>\n  " | ||||||
|  |         row += "\n  ".join(["<td>{}</th>".format(x) for x in v]) | ||||||
|  |         row += '\n  <td><a href="{}">{}</a></td>'.format(link, html_escape(link)) | ||||||
|  |         row += "\n</tr>" | ||||||
|  |         print(row) | ||||||
|  |  | ||||||
|  |     print("</table></body></html>") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										141
									
								
								bin/prisonparty.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										141
									
								
								bin/prisonparty.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # runs copyparty (or any other program really) in a chroot | ||||||
|  | # | ||||||
|  | # assumption: these directories, and everything within, are owned by root | ||||||
|  | sysdirs=(); for v in /bin /lib /lib32 /lib64 /sbin /usr /etc/alternatives ; do | ||||||
|  | 	[ -e $v ] && sysdirs+=($v) | ||||||
|  | done | ||||||
|  |  | ||||||
|  | # error-handler | ||||||
|  | help() { cat <<'EOF' | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   ./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- python3 copyparty-sfx.py [...] | ||||||
|  |  | ||||||
|  | example: | ||||||
|  |   ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 copyparty-sfx.py -v /mnt/nas/music::rwmd | ||||||
|  |  | ||||||
|  | example for running straight from source (instead of using an sfx): | ||||||
|  |   PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- python3 -um copyparty -v /mnt/nas/music::rwmd | ||||||
|  |  | ||||||
|  | note that if you have python modules installed as --user (such as bpm/key detectors), | ||||||
|  |   you should add /home/foo/.local as a VOLDIR | ||||||
|  |  | ||||||
|  | EOF | ||||||
|  | exit 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # read arguments | ||||||
|  | trap help EXIT | ||||||
|  | jail="$(realpath "$1")"; shift | ||||||
|  | uid="$1"; shift | ||||||
|  | gid="$1"; shift | ||||||
|  |  | ||||||
|  | vols=() | ||||||
|  | while true; do | ||||||
|  | 	v="$1"; shift | ||||||
|  | 	[ "$v" = -- ] && break  # end of volumes | ||||||
|  | 	[ "$#" -eq 0 ] && break  # invalid usage | ||||||
|  | 	vols+=( "$(realpath "$v" || echo "$v")" ) | ||||||
|  | done | ||||||
|  | pybin="$1"; shift | ||||||
|  | pybin="$(command -v "$pybin")" | ||||||
|  | pyarg= | ||||||
|  | while true; do | ||||||
|  | 	v="$1" | ||||||
|  | 	[ "${v:0:1}" = - ] || break | ||||||
|  | 	pyarg="$pyarg $v" | ||||||
|  | 	shift | ||||||
|  | done | ||||||
|  | cpp="$1"; shift | ||||||
|  | [ -d "$cpp" ] && cppdir="$PWD" || { | ||||||
|  | 	# sfx, not module | ||||||
|  | 	cpp="$(realpath "$cpp")" | ||||||
|  | 	cppdir="$(dirname "$cpp")" | ||||||
|  | } | ||||||
|  | trap - EXIT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # debug/vis | ||||||
|  | echo | ||||||
|  | echo "chroot-dir = $jail" | ||||||
|  | echo "user:group = $uid:$gid" | ||||||
|  | echo " copyparty = $cpp" | ||||||
|  | echo | ||||||
|  | printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:" | ||||||
|  | for v in "${vols[@]}"; do | ||||||
|  | 	printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v" | ||||||
|  | done | ||||||
|  | printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir" | ||||||
|  | printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD" | ||||||
|  | vols+=("$cppdir" "$PWD") | ||||||
|  | echo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # remove any trailing slashes | ||||||
|  | jail="${jail%/}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # bind-mount system directories and volumes | ||||||
|  | printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | sed -r 's`/$``' | LC_ALL=C sort | uniq | | ||||||
|  | while IFS= read -r v; do | ||||||
|  | 	[ -e "$v" ] || { | ||||||
|  | 		printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v" | ||||||
|  | 		continue | ||||||
|  | 	} | ||||||
|  | 	i1=$(stat -c%D.%i "$v"      2>/dev/null || echo a) | ||||||
|  | 	i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b) | ||||||
|  | 	# echo "v [$v] i1 [$i1] i2 [$i2]" | ||||||
|  | 	[ $i1 = $i2 ] && continue | ||||||
|  | 	 | ||||||
|  | 	mkdir -p "$jail$v" | ||||||
|  | 	mount --bind "$v" "$jail$v" | ||||||
|  | done | ||||||
|  |  | ||||||
|  |  | ||||||
|  | cln() { | ||||||
|  | 	rv=$? | ||||||
|  | 	wait -f -p rv $p || true | ||||||
|  | 	cd / | ||||||
|  | 	echo "stopping chroot..." | ||||||
|  | 	lsof "$jail" | grep -F "$jail" && | ||||||
|  | 		echo "chroot is in use; will not unmount" || | ||||||
|  | 	{ | ||||||
|  | 		mount | grep -F " on $jail" | | ||||||
|  | 		awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' | | ||||||
|  | 		LC_ALL=C sort -r  | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount | ||||||
|  | 	} | ||||||
|  | 	exit $rv | ||||||
|  | } | ||||||
|  | trap cln EXIT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # create a tmp | ||||||
|  | mkdir -p "$jail/tmp" | ||||||
|  | chmod 777 "$jail/tmp" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # create a dev | ||||||
|  | (cd $jail; mkdir -p dev; cd dev | ||||||
|  | [ -e null ]    || mknod -m 666 null    c 1 3 | ||||||
|  | [ -e zero ]    || mknod -m 666 zero    c 1 5 | ||||||
|  | [ -e random ]  || mknod -m 444 random  c 1 8 | ||||||
|  | [ -e urandom ] || mknod -m 444 urandom c 1 9 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # run copyparty | ||||||
|  | export HOME=$(getent passwd $uid | cut -d: -f6) | ||||||
|  | export USER=$(getent passwd $uid | cut -d: -f1) | ||||||
|  | export LOGNAME="$USER" | ||||||
|  | #echo "pybin [$pybin]" | ||||||
|  | #echo "pyarg [$pyarg]" | ||||||
|  | #echo "cpp [$cpp]" | ||||||
|  | chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" & | ||||||
|  | p=$! | ||||||
|  | trap 'kill -USR1 $p' USR1 | ||||||
|  | trap 'kill $p' INT TERM | ||||||
|  | wait | ||||||
							
								
								
									
										1194
									
								
								bin/u2c.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										1194
									
								
								bin/u2c.py
									
									
									
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										99
									
								
								bin/unforget.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										99
									
								
								bin/unforget.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | unforget.py: rebuild db from logfiles | ||||||
|  | 2022-09-07, v0.1, ed <irc.rizon.net>, MIT-Licensed | ||||||
|  | https://github.com/9001/copyparty/blob/hovudstraum/bin/unforget.py | ||||||
|  |  | ||||||
|  | only makes sense if running copyparty with --no-forget | ||||||
|  | (e.g. immediately shifting uploads to other storage) | ||||||
|  |  | ||||||
|  | usage: | ||||||
|  |   xz -d < log | ./unforget.py .hist/up2k.db | ||||||
|  |  | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import base64 | ||||||
|  | import sqlite3 | ||||||
|  | import argparse | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FS_ENCODING = sys.getfilesystemencoding() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | mem_cur = sqlite3.connect(":memory:").cursor() | ||||||
|  | mem_cur.execute(r"create table a (b text)") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def s3enc(rd: str, fn: str) -> tuple[str, str]: | ||||||
|  |     ret: list[str] = [] | ||||||
|  |     for v in [rd, fn]: | ||||||
|  |         try: | ||||||
|  |             mem_cur.execute("select * from a where b = ?", (v,)) | ||||||
|  |             ret.append(v) | ||||||
|  |         except: | ||||||
|  |             wtf8 = v.encode(FS_ENCODING, "surrogateescape") | ||||||
|  |             ret.append("//" + base64.urlsafe_b64encode(wtf8).decode("ascii")) | ||||||
|  |  | ||||||
|  |     return ret[0], ret[1] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     ap = argparse.ArgumentParser() | ||||||
|  |     ap.add_argument("db") | ||||||
|  |     ar = ap.parse_args() | ||||||
|  |  | ||||||
|  |     db = sqlite3.connect(ar.db).cursor() | ||||||
|  |     ptn_times = re.compile(r"no more chunks, setting times \(([0-9]+)") | ||||||
|  |     at = 0 | ||||||
|  |     ctr = 0 | ||||||
|  |  | ||||||
|  |     for ln in [x.decode("utf-8", "replace").rstrip() for x in sys.stdin.buffer]: | ||||||
|  |         if "no more chunks, setting times (" in ln: | ||||||
|  |             m = ptn_times.search(ln) | ||||||
|  |             if m: | ||||||
|  |                 at = int(m.group(1)) | ||||||
|  |  | ||||||
|  |         if '"hash": []' in ln: | ||||||
|  |             try: | ||||||
|  |                 ofs = ln.find("{") | ||||||
|  |                 j = json.loads(ln[ofs:]) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |             w = j["wark"] | ||||||
|  |             if db.execute("select w from up where w = ?", (w,)).fetchone(): | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             # PYTHONPATH=/home/ed/dev/copyparty/ python3 -m copyparty -e2dsa  -v foo:foo:rwmd,ed -aed:wark --no-forget | ||||||
|  |             # 05:34:43.845 127.0.0.1 42496       no more chunks, setting times (1662528883, 1658001882) | ||||||
|  |             # 05:34:43.863 127.0.0.1 42496       {"name": "f\"2", "purl": "/foo/bar/baz/", "size": 1674, "lmod": 1658001882, "sprs": true, "hash": [], "wark": "LKIWpp2jEAh9dH3fu-DobuURFGEKlODXDGTpZ1otMhUg"} | ||||||
|  |             # |                      w                       |     mt     |  sz  |   rd    | fn  |    ip     |     at     | | ||||||
|  |             # | LKIWpp2jEAh9dH3fu-DobuURFGEKlODXDGTpZ1otMhUg | 1658001882 | 1674 | bar/baz | f"2 | 127.0.0.1 | 1662528883 | | ||||||
|  |  | ||||||
|  |             rd, fn = s3enc(j["purl"].strip("/"), j["name"]) | ||||||
|  |             ip = ln.split(" ")[1].split("m")[-1] | ||||||
|  |  | ||||||
|  |             q = "insert into up values (?,?,?,?,?,?,?)" | ||||||
|  |             v = (w, int(j["lmod"]), int(j["size"]), rd, fn, ip, at) | ||||||
|  |             db.execute(q, v) | ||||||
|  |             ctr += 1 | ||||||
|  |             if ctr % 1024 == 1023: | ||||||
|  |                 print(f"{ctr} commit...") | ||||||
|  |                 db.connection.commit() | ||||||
|  |  | ||||||
|  |     if ctr: | ||||||
|  |         db.connection.commit() | ||||||
|  |  | ||||||
|  |     print(f"unforgot {ctr} files") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										24
									
								
								bin/up2k.sh
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										24
									
								
								bin/up2k.sh
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -8,7 +8,7 @@ set -e | |||||||
| ## | ## | ||||||
| ## config | ## config | ||||||
|  |  | ||||||
| datalen=$((2*1024*1024*1024)) | datalen=$((128*1024*1024)) | ||||||
| target=127.0.0.1 | target=127.0.0.1 | ||||||
| posturl=/inc | posturl=/inc | ||||||
| passwd=wark | passwd=wark | ||||||
| @@ -37,10 +37,10 @@ gendata() { | |||||||
| # pipe a chunk, get the base64 checksum | # pipe a chunk, get the base64 checksum | ||||||
| gethash() { | gethash() { | ||||||
|     printf $( |     printf $( | ||||||
|         sha512sum | cut -c-64 | |         sha512sum | cut -c-66 | | ||||||
|         sed -r 's/ .*//;s/(..)/\\x\1/g' |         sed -r 's/ .*//;s/(..)/\\x\1/g' | ||||||
|     ) | |     ) | | ||||||
|     base64 -w0 | cut -c-43 | |     base64 -w0 | cut -c-44 | | ||||||
|     tr '+/' '-_' |     tr '+/' '-_' | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -123,7 +123,7 @@ printf '\033[36m' | |||||||
| { | { | ||||||
|     { |     { | ||||||
|         cat <<EOF |         cat <<EOF | ||||||
| POST $posturl/handshake.php HTTP/1.1 | POST $posturl/ HTTP/1.1 | ||||||
| Connection: Close | Connection: Close | ||||||
| Cookie: cppwd=$passwd | Cookie: cppwd=$passwd | ||||||
| Content-Type: text/plain;charset=UTF-8 | Content-Type: text/plain;charset=UTF-8 | ||||||
| @@ -145,14 +145,16 @@ printf '\033[0m\nwark: %s\n' $wark | |||||||
| ## | ## | ||||||
| ## wait for signal to continue | ## wait for signal to continue | ||||||
|  |  | ||||||
| w8=/dev/shm/$salt.w8 | true || { | ||||||
| touch $w8 |     w8=/dev/shm/$salt.w8 | ||||||
|  |     touch $w8 | ||||||
|  |  | ||||||
| echo "ready;  rm -f $w8" |     echo "ready;  rm -f $w8" | ||||||
|  |  | ||||||
| while [ -e $w8 ]; do |     while [ -e $w8 ]; do | ||||||
|     sleep 0.2 |         sleep 0.2 | ||||||
| done |     done | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| ## | ## | ||||||
| @@ -175,7 +177,7 @@ while [ $remains -gt 0 ]; do | |||||||
|      |      | ||||||
|     { |     { | ||||||
|         cat <<EOF |         cat <<EOF | ||||||
| POST $posturl/chunkpit.php HTTP/1.1 | POST $posturl/ HTTP/1.1 | ||||||
| Connection: Keep-Alive | Connection: Keep-Alive | ||||||
| Cookie: cppwd=$passwd | Cookie: cppwd=$passwd | ||||||
| Content-Type: application/octet-stream | Content-Type: application/octet-stream | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | ### [`plugins/`](plugins/) | ||||||
|  | * example extensions | ||||||
|  |  | ||||||
| ### [`copyparty.bat`](copyparty.bat) | ### [`copyparty.bat`](copyparty.bat) | ||||||
| * launches copyparty with no arguments (anon read+write within same folder) | * launches copyparty with no arguments (anon read+write within same folder) | ||||||
| * intended for windows machines with no python.exe in PATH | * intended for windows machines with no python.exe in PATH | ||||||
| @@ -19,17 +22,29 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share | |||||||
| * `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$` | * `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$` | ||||||
| * `pw`: password (remove `Parameters` if anon-write) | * `pw`: password (remove `Parameters` if anon-write) | ||||||
|  |  | ||||||
|  | ### [`media-osd-bgone.ps1`](media-osd-bgone.ps1) | ||||||
|  | * disables the [windows OSD popup](https://user-images.githubusercontent.com/241032/122821375-0e08df80-d2dd-11eb-9fd9-184e8aacf1d0.png) (the thing on the left) which appears every time you hit media hotkeys to adjust volume or change song while playing music with the copyparty web-ui, or most other audio players really | ||||||
|  |  | ||||||
| ### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg) | ### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg) | ||||||
| * disables thumbnails and folder-type detection in windows explorer | * disables thumbnails and folder-type detection in windows explorer | ||||||
| * makes it way faster (especially for slow/networked locations (such as copyparty-fuse)) | * makes it way faster (especially for slow/networked locations (such as partyfuse)) | ||||||
|  |  | ||||||
|  | ### [`webdav-cfg.reg`](webdav-cfg.bat) | ||||||
|  | * improves the native webdav support in windows; | ||||||
|  |   * removes the 47.6 MiB filesize limit when downloading from webdav | ||||||
|  |   * optionally enables webdav basic-auth over plaintext http | ||||||
|  |   * optionally helps disable wpad, removing the 10sec latency | ||||||
|  |  | ||||||
| ### [`cfssl.sh`](cfssl.sh) | ### [`cfssl.sh`](cfssl.sh) | ||||||
| * creates CA and server certificates using cfssl | * creates CA and server certificates using cfssl | ||||||
| * give a 3rd argument to install it to your copyparty config | * give a 3rd argument to install it to your copyparty config | ||||||
|  | * systemd service at [`systemd/cfssl.service`](systemd/cfssl.service) | ||||||
|  |  | ||||||
| # OS integration | # OS integration | ||||||
| init-scripts to start copyparty as a service | init-scripts to start copyparty as a service | ||||||
| * [`systemd/copyparty.service`](systemd/copyparty.service) | * [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally | ||||||
|  | * [`rc/copyparty`](rc/copyparty) runs sfx normally on freebsd, create a `copyparty` user | ||||||
|  | * [`systemd/prisonparty.service`](systemd/prisonparty.service) runs the sfx in a chroot | ||||||
| * [`openrc/copyparty`](openrc/copyparty) | * [`openrc/copyparty`](openrc/copyparty) | ||||||
|  |  | ||||||
| # Reverse-proxy | # Reverse-proxy | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								contrib/apache/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								contrib/apache/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | # when running copyparty behind a reverse proxy, | ||||||
|  | # the following arguments are recommended: | ||||||
|  | # | ||||||
|  | #   -i 127.0.0.1    only accept connections from nginx | ||||||
|  | # | ||||||
|  | # if you are doing location-based proxying (such as `/stuff` below) | ||||||
|  | # you must run copyparty with --rp-loc=stuff | ||||||
|  | # | ||||||
|  | # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 | ||||||
|  |  | ||||||
|  | LoadModule proxy_module modules/mod_proxy.so | ||||||
|  | ProxyPass "/stuff" "http://127.0.0.1:3923/stuff" | ||||||
|  | # do not specify ProxyPassReverse | ||||||
|  | RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} | ||||||
| @@ -1,13 +1,44 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
| # ca-name and server-name | cat >/dev/null <<'EOF' | ||||||
| ca_name="$1" |  | ||||||
| srv_name="$2" |  | ||||||
|  |  | ||||||
| [ -z "$srv_name" ] && { | NOTE: copyparty is now able to do this automatically; | ||||||
| 	echo "need arg 1: ca name" | however you may wish to use this script instead if | ||||||
| 	echo "need arg 2: server name" | you have specific needs (or if copyparty breaks) | ||||||
|  |  | ||||||
|  | this script generates a new self-signed TLS certificate and | ||||||
|  | replaces the default insecure one that comes with copyparty | ||||||
|  |  | ||||||
|  | as it is trivial to impersonate a copyparty server using the | ||||||
|  | default certificate, it is highly recommended to do this | ||||||
|  |  | ||||||
|  | this will create a self-signed CA, and a Server certificate | ||||||
|  | which gets signed by that CA -- you can run it multiple times | ||||||
|  | with different server-FQDNs / IPs to create additional certs | ||||||
|  | for all your different servers / (non-)copyparty services | ||||||
|  |  | ||||||
|  | EOF | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # ca-name and server-fqdn | ||||||
|  | ca_name="$1" | ||||||
|  | srv_fqdn="$2" | ||||||
|  |  | ||||||
|  | [ -z "$srv_fqdn" ] && { cat <<'EOF' | ||||||
|  | need arg 1: ca name | ||||||
|  | need arg 2: server fqdn and/or IPs, comma-separated | ||||||
|  | optional arg 3: if set, write cert into copyparty cfg | ||||||
|  |  | ||||||
|  | example: | ||||||
|  |   ./cfssl.sh PartyCo partybox.local y | ||||||
|  | EOF | ||||||
|  | 	exit 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | command -v cfssljson 2>/dev/null || { | ||||||
|  | 	echo please install cfssl and try again | ||||||
| 	exit 1 | 	exit 1 | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -31,15 +62,15 @@ EOF | |||||||
| gen_srv() { | gen_srv() { | ||||||
| 	(tee /dev/stderr <<EOF | 	(tee /dev/stderr <<EOF | ||||||
| {"key": {"algo":"rsa", "size":4096}, | {"key": {"algo":"rsa", "size":4096}, | ||||||
| "names": [{"O":"$ca_name - $srv_name"}]} | "names": [{"O":"$ca_name - $srv_fqdn"}]} | ||||||
| EOF | EOF | ||||||
| 	)| | 	)| | ||||||
| 	cfssl gencert -ca ca.pem -ca-key ca.key \ | 	cfssl gencert -ca ca.pem -ca-key ca.key \ | ||||||
| 		-profile=www -hostname="$srv_name.$ca_name" - | | 		-profile=www -hostname="$srv_fqdn" - | | ||||||
| 	cfssljson -bare "$srv_name" | 	cfssljson -bare "$srv_fqdn" | ||||||
|  |  | ||||||
| 	mv "$srv_name-key.pem" "$srv_name.key" | 	mv "$srv_fqdn-key.pem" "$srv_fqdn.key" | ||||||
| 	rm "$srv_name.csr" | 	rm "$srv_fqdn.csr" | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -57,13 +88,15 @@ show() { | |||||||
| 	awk '!o; {o=0} /[0-9a-f:]{16}/{o=1}' | 	awk '!o; {o=0} /[0-9a-f:]{16}/{o=1}' | ||||||
| } | } | ||||||
| show ca.pem | show ca.pem | ||||||
| show "$srv_name.pem" | show "$srv_fqdn.pem" | ||||||
|  | echo | ||||||
|  | echo "successfully generated new certificates" | ||||||
|  |  | ||||||
| # write cert into copyparty config | # write cert into copyparty config | ||||||
| [ -z "$3" ] || { | [ -z "$3" ] || { | ||||||
| 	mkdir -p ~/.config/copyparty | 	mkdir -p ~/.config/copyparty | ||||||
| 	cat "$srv_name".{key,pem} ca.pem >~/.config/copyparty/cert.pem  | 	cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem  | ||||||
|  | 	echo "successfully replaced copyparty certificate" | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|  |  | ||||||
| <head> | <head> | ||||||
| 	<meta charset="utf-8"> | 	<meta charset="utf-8"> | ||||||
| 	<title>⇆🎉 redirect</title> | 	<title>💾🎉 redirect</title> | ||||||
| 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||||
| 	<style> | 	<style> | ||||||
|  |  | ||||||
| @@ -26,8 +26,8 @@ a { | |||||||
| 	<script> | 	<script> | ||||||
|  |  | ||||||
| var a = document.getElementById('redir'), | var a = document.getElementById('redir'), | ||||||
| 	proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http', | 	proto = location.protocol.indexOf('https') === 0 ? 'https' : 'http', | ||||||
| 	loc = window.location.hostname || '127.0.0.1', | 	loc = location.hostname || '127.0.0.1', | ||||||
| 	port = a.getAttribute('href').split(':').pop().split('/')[0], | 	port = a.getAttribute('href').split(':').pop().split('/')[0], | ||||||
| 	url = proto + '://' + loc + ':' + port + '/'; | 	url = proto + '://' + loc + ':' + port + '/'; | ||||||
|  |  | ||||||
| @@ -35,7 +35,7 @@ a.setAttribute('href', url); | |||||||
| document.getElementById('desc').innerHTML = 'redirecting to'; | document.getElementById('desc').innerHTML = 'redirecting to'; | ||||||
|  |  | ||||||
| setTimeout(function() { | setTimeout(function() { | ||||||
| 	window.location.href = url; | 	location.href = url; | ||||||
| }, 500); | }, 500); | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								contrib/ios/upload-to-copyparty.shortcut
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								contrib/ios/upload-to-copyparty.shortcut
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										104
									
								
								contrib/media-osd-bgone.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								contrib/media-osd-bgone.ps1
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | # media-osd-bgone.ps1: disable media-control OSD on win10do | ||||||
|  | # v1.1, 2021-06-25, ed <irc.rizon.net>, MIT-licensed | ||||||
|  | # https://github.com/9001/copyparty/blob/hovudstraum/contrib/media-osd-bgone.ps1 | ||||||
|  | # | ||||||
|  | # locates the first window that looks like the media OSD and minimizes it; | ||||||
|  | # doing this once after each reboot should do the trick | ||||||
|  | # (adjust the width/height filter if it doesn't work) | ||||||
|  | # | ||||||
|  | # --------------------------------------------------------------------- | ||||||
|  | # | ||||||
|  | # tip: save the following as "media-osd-bgone.bat" next to this script: | ||||||
|  | #   start cmd /c "powershell -command ""set-executionpolicy -scope process bypass; .\media-osd-bgone.ps1"" & ping -n 2 127.1 >nul" | ||||||
|  | # | ||||||
|  | # then create a shortcut to that bat-file and move the shortcut here: | ||||||
|  | #   %appdata%\Microsoft\Windows\Start Menu\Programs\Startup | ||||||
|  | # | ||||||
|  | # and now this will autorun on bootup | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Add-Type -TypeDefinition @" | ||||||
|  | using System; | ||||||
|  | using System.IO; | ||||||
|  | using System.Threading; | ||||||
|  | using System.Diagnostics; | ||||||
|  | using System.Runtime.InteropServices; | ||||||
|  | using System.Windows.Forms; | ||||||
|  |  | ||||||
|  | namespace A { | ||||||
|  |   public class B : Control { | ||||||
|  |  | ||||||
|  |     [DllImport("user32.dll")] | ||||||
|  |     static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); | ||||||
|  |  | ||||||
|  |     [DllImport("user32.dll", SetLastError = true)] | ||||||
|  |     static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); | ||||||
|  |  | ||||||
|  |     [DllImport("user32.dll", SetLastError=true)] | ||||||
|  |     static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); | ||||||
|  |  | ||||||
|  |     [DllImport("user32.dll")] | ||||||
|  |     static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); | ||||||
|  |  | ||||||
|  |     [StructLayout(LayoutKind.Sequential)] | ||||||
|  |     public struct RECT { | ||||||
|  |       public int x; | ||||||
|  |       public int y; | ||||||
|  |       public int x2; | ||||||
|  |       public int y2; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     bool fa() { | ||||||
|  |       RECT r; | ||||||
|  |       IntPtr it = IntPtr.Zero; | ||||||
|  |       while ((it = FindWindowEx(IntPtr.Zero, it, "NativeHWNDHost", "")) != IntPtr.Zero) { | ||||||
|  |         if (FindWindowEx(it, IntPtr.Zero, "DirectUIHWND", "") == IntPtr.Zero) | ||||||
|  |           continue; | ||||||
|  |          | ||||||
|  |         if (!GetWindowRect(it, out r)) | ||||||
|  |           continue; | ||||||
|  |  | ||||||
|  |         int w = r.x2 - r.x + 1; | ||||||
|  |         int h = r.y2 - r.y + 1; | ||||||
|  |  | ||||||
|  |         Console.WriteLine("[*] hwnd {0:x} @ {1}x{2} sz {3}x{4}", it, r.x, r.y, w, h); | ||||||
|  |         if (h != 141) | ||||||
|  |           continue; | ||||||
|  |          | ||||||
|  |         ShowWindow(it, 6); | ||||||
|  |         Console.WriteLine("[+] poof"); | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void fb() { | ||||||
|  |       keybd_event((byte)Keys.VolumeMute, 0, 0, 0); | ||||||
|  |       keybd_event((byte)Keys.VolumeMute, 0, 2, 0); | ||||||
|  |       Thread.Sleep(500); | ||||||
|  |       keybd_event((byte)Keys.VolumeMute, 0, 0, 0); | ||||||
|  |       keybd_event((byte)Keys.VolumeMute, 0, 2, 0); | ||||||
|  |  | ||||||
|  |       while (true) { | ||||||
|  |         if (fa()) { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         Console.WriteLine("[!] not found"); | ||||||
|  |         Thread.Sleep(1000); | ||||||
|  |       } | ||||||
|  |       this.Invoke((MethodInvoker)delegate { | ||||||
|  |         Application.Exit(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void Run() { | ||||||
|  |       Console.WriteLine("[+] hi"); | ||||||
|  |       new Thread(new ThreadStart(fb)).Start(); | ||||||
|  |       Application.Run(); | ||||||
|  |       Console.WriteLine("[+] bye"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | "@ -ReferencedAssemblies System.Windows.Forms | ||||||
|  |  | ||||||
|  | (New-Object -TypeName A.B).Run() | ||||||
| @@ -1,19 +1,20 @@ | |||||||
| # when running copyparty behind a reverse proxy, | # when running copyparty behind a reverse proxy, | ||||||
| # the following arguments are recommended: | # the following arguments are recommended: | ||||||
| # | # | ||||||
| #   -nc 512         important, see next paragraph |  | ||||||
| #   --http-only     lower latency on initial connection |  | ||||||
| #   -i 127.0.0.1    only accept connections from nginx | #   -i 127.0.0.1    only accept connections from nginx | ||||||
| # | # | ||||||
| # -nc must match or exceed the webserver's max number of concurrent clients; | # -nc must match or exceed the webserver's max number of concurrent clients; | ||||||
|  | # copyparty default is 1024 if OS permits it (see "max clients:" on startup), | ||||||
| # nginx default is 512  (worker_processes 1, worker_connections 512) | # nginx default is 512  (worker_processes 1, worker_connections 512) | ||||||
| # | # | ||||||
| # you may also consider adding -j0 for CPU-intensive configurations | # you may also consider adding -j0 for CPU-intensive configurations | ||||||
| # (not that i can really think of any good examples) | # (5'000 requests per second, or 20gbps upload/download in parallel) | ||||||
|  | # | ||||||
|  | # on fedora/rhel, remember to setsebool -P httpd_can_network_connect 1 | ||||||
|  |  | ||||||
| upstream cpp { | upstream cpp { | ||||||
| 	server 127.0.0.1:3923; | 	server 127.0.0.1:3923; | ||||||
| 	keepalive 120; | 	keepalive 1; | ||||||
| } | } | ||||||
| server { | server { | ||||||
| 	listen 443 ssl; | 	listen 443 ssl; | ||||||
| @@ -33,7 +34,15 @@ server { | |||||||
| 		proxy_set_header   Host              $host; | 		proxy_set_header   Host              $host; | ||||||
| 		proxy_set_header   X-Real-IP         $remote_addr; | 		proxy_set_header   X-Real-IP         $remote_addr; | ||||||
| 		proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for; | 		proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for; | ||||||
|  | 		# NOTE: with cloudflare you want this instead: | ||||||
|  | 		#proxy_set_header   X-Forwarded-For   $http_cf_connecting_ip; | ||||||
| 		proxy_set_header   X-Forwarded-Proto $scheme; | 		proxy_set_header   X-Forwarded-Proto $scheme; | ||||||
| 		proxy_set_header   Connection        "Keep-Alive"; | 		proxy_set_header   Connection        "Keep-Alive"; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | # default client_max_body_size (1M) blocks uploads larger than 256 MiB | ||||||
|  | client_max_body_size 1024M; | ||||||
|  | client_header_timeout 610m; | ||||||
|  | client_body_timeout 610m; | ||||||
|  | send_timeout 610m; | ||||||
|   | |||||||
							
								
								
									
										283
									
								
								contrib/nixos/modules/copyparty.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								contrib/nixos/modules/copyparty.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | |||||||
|  | { config, pkgs, lib, ... }: | ||||||
|  |  | ||||||
|  | with lib; | ||||||
|  |  | ||||||
|  | let | ||||||
|  |   mkKeyValue = key: value: | ||||||
|  |     if value == true then | ||||||
|  |     # sets with a true boolean value are coerced to just the key name | ||||||
|  |       key | ||||||
|  |     else if value == false then | ||||||
|  |     # or omitted completely when false | ||||||
|  |       "" | ||||||
|  |     else | ||||||
|  |       (generators.mkKeyValueDefault { inherit mkValueString; } ": " key value); | ||||||
|  |  | ||||||
|  |   mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value); | ||||||
|  |  | ||||||
|  |   mkValueString = value: | ||||||
|  |     if isList value then | ||||||
|  |       (concatStringsSep ", " (map mkValueString value)) | ||||||
|  |     else if isAttrs value then | ||||||
|  |       "\n" + (mkAttrsString value) | ||||||
|  |     else | ||||||
|  |       (generators.mkValueStringDefault { } value); | ||||||
|  |  | ||||||
|  |   mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]"; | ||||||
|  |  | ||||||
|  |   mkSection = name: attrs: '' | ||||||
|  |     ${mkSectionName name} | ||||||
|  |     ${mkAttrsString attrs} | ||||||
|  |   ''; | ||||||
|  |  | ||||||
|  |   mkVolume = name: attrs: '' | ||||||
|  |     ${mkSectionName name} | ||||||
|  |     ${attrs.path} | ||||||
|  |     ${mkAttrsString { | ||||||
|  |       accs = attrs.access; | ||||||
|  |       flags = attrs.flags; | ||||||
|  |     }} | ||||||
|  |   ''; | ||||||
|  |  | ||||||
|  |   passwordPlaceholder = name: "{{password-${name}}}"; | ||||||
|  |  | ||||||
|  |   accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name); | ||||||
|  |  | ||||||
|  |   configStr = '' | ||||||
|  |     ${mkSection "global" cfg.settings} | ||||||
|  |     ${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)} | ||||||
|  |     ${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)} | ||||||
|  |   ''; | ||||||
|  |  | ||||||
|  |   name = "copyparty"; | ||||||
|  |   cfg = config.services.copyparty; | ||||||
|  |   configFile = pkgs.writeText "${name}.conf" configStr; | ||||||
|  |   runtimeConfigPath = "/run/${name}/${name}.conf"; | ||||||
|  |   home = "/var/lib/${name}"; | ||||||
|  |   defaultShareDir = "${home}/data"; | ||||||
|  | in { | ||||||
|  |   options.services.copyparty = { | ||||||
|  |     enable = mkEnableOption "web-based file manager"; | ||||||
|  |  | ||||||
|  |     package = mkOption { | ||||||
|  |       type = types.package; | ||||||
|  |       default = pkgs.copyparty; | ||||||
|  |       defaultText = "pkgs.copyparty"; | ||||||
|  |       description = '' | ||||||
|  |         Package of the application to run, exposed for overriding purposes. | ||||||
|  |       ''; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     openFilesLimit = mkOption { | ||||||
|  |       default = 4096; | ||||||
|  |       type = types.either types.int types.str; | ||||||
|  |       description = "Number of files to allow copyparty to open."; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     settings = mkOption { | ||||||
|  |       type = types.attrs; | ||||||
|  |       description = '' | ||||||
|  |         Global settings to apply. | ||||||
|  |         Directly maps to values in the [global] section of the copyparty config. | ||||||
|  |         See `${getExe cfg.package} --help` for more details. | ||||||
|  |       ''; | ||||||
|  |       default = { | ||||||
|  |         i = "127.0.0.1"; | ||||||
|  |         no-reload = true; | ||||||
|  |       }; | ||||||
|  |       example = literalExpression '' | ||||||
|  |         { | ||||||
|  |           i = "0.0.0.0"; | ||||||
|  |           no-reload = true; | ||||||
|  |         } | ||||||
|  |       ''; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     accounts = mkOption { | ||||||
|  |       type = types.attrsOf (types.submodule ({ ... }: { | ||||||
|  |         options = { | ||||||
|  |           passwordFile = mkOption { | ||||||
|  |             type = types.str; | ||||||
|  |             description = '' | ||||||
|  |               Runtime file path to a file containing the user password. | ||||||
|  |               Must be readable by the copyparty user. | ||||||
|  |             ''; | ||||||
|  |             example = "/run/keys/copyparty/ed"; | ||||||
|  |           }; | ||||||
|  |         }; | ||||||
|  |       })); | ||||||
|  |       description = '' | ||||||
|  |         A set of copyparty accounts to create. | ||||||
|  |       ''; | ||||||
|  |       default = { }; | ||||||
|  |       example = literalExpression '' | ||||||
|  |         { | ||||||
|  |           ed.passwordFile = "/run/keys/copyparty/ed"; | ||||||
|  |         }; | ||||||
|  |       ''; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     volumes = mkOption { | ||||||
|  |       type = types.attrsOf (types.submodule ({ ... }: { | ||||||
|  |         options = { | ||||||
|  |           path = mkOption { | ||||||
|  |             type = types.str; | ||||||
|  |             description = '' | ||||||
|  |               Path of a directory to share. | ||||||
|  |             ''; | ||||||
|  |           }; | ||||||
|  |           access = mkOption { | ||||||
|  |             type = types.attrs; | ||||||
|  |             description = '' | ||||||
|  |               Attribute list of permissions and the users to apply them to. | ||||||
|  |  | ||||||
|  |               The key must be a string containing any combination of allowed permission: | ||||||
|  |                 "r" (read):   list folder contents, download files | ||||||
|  |                 "w" (write):  upload files; need "r" to see the uploads | ||||||
|  |                 "m" (move):   move files and folders; need "w" at destination | ||||||
|  |                 "d" (delete): permanently delete files and folders | ||||||
|  |                 "g" (get):    download files, but cannot see folder contents | ||||||
|  |                 "G" (upget):  "get", but can see filekeys of their own uploads | ||||||
|  |                 "h" (html):   "get", but folders return their index.html | ||||||
|  |                 "a" (admin):  can see uploader IPs, config-reload | ||||||
|  |  | ||||||
|  |               For example: "rwmd" | ||||||
|  |  | ||||||
|  |               The value must be one of: | ||||||
|  |                 an account name, defined in `accounts` | ||||||
|  |                 a list of account names | ||||||
|  |                 "*", which means "any account" | ||||||
|  |             ''; | ||||||
|  |             example = literalExpression '' | ||||||
|  |               { | ||||||
|  |                 # wG = write-upget = see your own uploads only | ||||||
|  |                 wG = "*"; | ||||||
|  |                 # read-write-modify-delete for users "ed" and "k" | ||||||
|  |                 rwmd = ["ed" "k"]; | ||||||
|  |               }; | ||||||
|  |             ''; | ||||||
|  |           }; | ||||||
|  |           flags = mkOption { | ||||||
|  |             type = types.attrs; | ||||||
|  |             description = '' | ||||||
|  |               Attribute list of volume flags to apply. | ||||||
|  |               See `${getExe cfg.package} --help-flags` for more details. | ||||||
|  |             ''; | ||||||
|  |             example = literalExpression '' | ||||||
|  |               { | ||||||
|  |                 # "fk" enables filekeys (necessary for upget permission) (4 chars long) | ||||||
|  |                 fk = 4; | ||||||
|  |                 # scan for new files every 60sec | ||||||
|  |                 scan = 60; | ||||||
|  |                 # volflag "e2d" enables the uploads database | ||||||
|  |                 e2d = true; | ||||||
|  |                 # "d2t" disables multimedia parsers (in case the uploads are malicious) | ||||||
|  |                 d2t = true; | ||||||
|  |                 # skips hashing file contents if path matches *.iso | ||||||
|  |                 nohash = "\.iso$"; | ||||||
|  |               }; | ||||||
|  |             ''; | ||||||
|  |             default = { }; | ||||||
|  |           }; | ||||||
|  |         }; | ||||||
|  |       })); | ||||||
|  |       description = "A set of copyparty volumes to create"; | ||||||
|  |       default = { | ||||||
|  |         "/" = { | ||||||
|  |           path = defaultShareDir; | ||||||
|  |           access = { r = "*"; }; | ||||||
|  |         }; | ||||||
|  |       }; | ||||||
|  |       example = literalExpression '' | ||||||
|  |         { | ||||||
|  |           "/" = { | ||||||
|  |             path = ${defaultShareDir}; | ||||||
|  |             access = { | ||||||
|  |               # wG = write-upget = see your own uploads only | ||||||
|  |               wG = "*"; | ||||||
|  |               # read-write-modify-delete for users "ed" and "k" | ||||||
|  |               rwmd = ["ed" "k"]; | ||||||
|  |             }; | ||||||
|  |           }; | ||||||
|  |         }; | ||||||
|  |       ''; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   config = mkIf cfg.enable { | ||||||
|  |     systemd.services.copyparty = { | ||||||
|  |       description = "http file sharing hub"; | ||||||
|  |       wantedBy = [ "multi-user.target" ]; | ||||||
|  |  | ||||||
|  |       environment = { | ||||||
|  |         PYTHONUNBUFFERED = "true"; | ||||||
|  |         XDG_CONFIG_HOME = "${home}/.config"; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       preStart = let | ||||||
|  |         replaceSecretCommand = name: attrs: | ||||||
|  |           "${getExe pkgs.replace-secret} '${ | ||||||
|  |             passwordPlaceholder name | ||||||
|  |           }' '${attrs.passwordFile}' ${runtimeConfigPath}"; | ||||||
|  |       in '' | ||||||
|  |         set -euo pipefail | ||||||
|  |         install -m 600 ${configFile} ${runtimeConfigPath} | ||||||
|  |         ${concatStringsSep "\n" | ||||||
|  |         (mapAttrsToList replaceSecretCommand cfg.accounts)} | ||||||
|  |       ''; | ||||||
|  |  | ||||||
|  |       serviceConfig = { | ||||||
|  |         Type = "simple"; | ||||||
|  |         ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}"; | ||||||
|  |  | ||||||
|  |         # Hardening options | ||||||
|  |         User = "copyparty"; | ||||||
|  |         Group = "copyparty"; | ||||||
|  |         RuntimeDirectory = name; | ||||||
|  |         RuntimeDirectoryMode = "0700"; | ||||||
|  |         StateDirectory = [ name "${name}/data" "${name}/.config" ]; | ||||||
|  |         StateDirectoryMode = "0700"; | ||||||
|  |         WorkingDirectory = home; | ||||||
|  |         TemporaryFileSystem = "/:ro"; | ||||||
|  |         BindReadOnlyPaths = [ | ||||||
|  |           "/nix/store" | ||||||
|  |           "-/etc/resolv.conf" | ||||||
|  |           "-/etc/nsswitch.conf" | ||||||
|  |           "-/etc/hosts" | ||||||
|  |           "-/etc/localtime" | ||||||
|  |         ] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts); | ||||||
|  |         BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes); | ||||||
|  |         # Would re-mount paths ignored by temporary root | ||||||
|  |         #ProtectSystem = "strict"; | ||||||
|  |         ProtectHome = true; | ||||||
|  |         PrivateTmp = true; | ||||||
|  |         PrivateDevices = true; | ||||||
|  |         ProtectKernelTunables = true; | ||||||
|  |         ProtectControlGroups = true; | ||||||
|  |         RestrictSUIDSGID = true; | ||||||
|  |         PrivateMounts = true; | ||||||
|  |         ProtectKernelModules = true; | ||||||
|  |         ProtectKernelLogs = true; | ||||||
|  |         ProtectHostname = true; | ||||||
|  |         ProtectClock = true; | ||||||
|  |         ProtectProc = "invisible"; | ||||||
|  |         ProcSubset = "pid"; | ||||||
|  |         RestrictNamespaces = true; | ||||||
|  |         RemoveIPC = true; | ||||||
|  |         UMask = "0077"; | ||||||
|  |         LimitNOFILE = cfg.openFilesLimit; | ||||||
|  |         NoNewPrivileges = true; | ||||||
|  |         LockPersonality = true; | ||||||
|  |         RestrictRealtime = true; | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     users.groups.copyparty = { }; | ||||||
|  |     users.users.copyparty = { | ||||||
|  |       description = "Service user for copyparty"; | ||||||
|  |       group = "copyparty"; | ||||||
|  |       home = home; | ||||||
|  |       isSystemUser = true; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | } | ||||||
| @@ -8,11 +8,11 @@ | |||||||
| # | # | ||||||
| # you may want to: | # you may want to: | ||||||
| #   change '/usr/bin/python' to another interpreter | #   change '/usr/bin/python' to another interpreter | ||||||
| #   change '/mnt::a' to another location or permission-set | #   change '/mnt::rw' to another location or permission-set | ||||||
|  |  | ||||||
| name="$SVCNAME" | name="$SVCNAME" | ||||||
| command_background=true | command_background=true | ||||||
| pidfile="/var/run/$SVCNAME.pid" | pidfile="/var/run/$SVCNAME.pid" | ||||||
|  |  | ||||||
| command="/usr/bin/python /usr/local/bin/copyparty-sfx.py" | command="/usr/bin/python3 /usr/local/bin/copyparty-sfx.py" | ||||||
| command_args="-q -v /mnt::a" | command_args="-q -v /mnt::rw" | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								contrib/package/arch/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								contrib/package/arch/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | # Maintainer: icxes <dev.null@need.moe> | ||||||
|  | pkgname=copyparty | ||||||
|  | pkgver="1.9.5" | ||||||
|  | pkgrel=1 | ||||||
|  | pkgdesc="Portable file sharing hub" | ||||||
|  | arch=("any") | ||||||
|  | url="https://github.com/9001/${pkgname}" | ||||||
|  | license=('MIT') | ||||||
|  | depends=("python" "lsof" "python-jinja") | ||||||
|  | makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz") | ||||||
|  | optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" | ||||||
|  |             "python-mutagen: music tags (alternative)"  | ||||||
|  |             "python-pillow: thumbnails for images"  | ||||||
|  |             "python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"  | ||||||
|  |             "libkeyfinder-git: detection of musical keys"  | ||||||
|  |             "qm-vamp-plugins: BPM detection"  | ||||||
|  |             "python-pyopenssl: ftps functionality"  | ||||||
|  |             "python-argon2_cffi: hashed passwords in config"  | ||||||
|  |             "python-impacket-git: smb support (bad idea)" | ||||||
|  | ) | ||||||
|  | source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") | ||||||
|  | backup=("etc/${pkgname}.d/init" ) | ||||||
|  | sha256sums=("0c68368bab5b17716860994b3e6485e4d396d0ec3eb5f1c8947eca8cb2b18821") | ||||||
|  |  | ||||||
|  | build() { | ||||||
|  |     cd "${srcdir}/${pkgname}-${pkgver}" | ||||||
|  |      | ||||||
|  |     pushd copyparty/web | ||||||
|  |     make -j$(nproc) | ||||||
|  |     rm Makefile | ||||||
|  |     popd | ||||||
|  |      | ||||||
|  |     python3 -m build -wn | ||||||
|  | } | ||||||
|  |  | ||||||
|  | package() { | ||||||
|  |     cd "${srcdir}/${pkgname}-${pkgver}" | ||||||
|  |     python3 -m installer -d "$pkgdir" dist/*.whl | ||||||
|  |  | ||||||
|  |     install -dm755 "${pkgdir}/etc/${pkgname}.d" | ||||||
|  |     install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty" | ||||||
|  |     install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init" | ||||||
|  |     install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service" | ||||||
|  |     install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service" | ||||||
|  |     install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md" | ||||||
|  |     install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" | ||||||
|  |  | ||||||
|  |     find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return | ||||||
|  |     echo "┏━━━━━━━━━━━━━━━──-" | ||||||
|  |     echo "┃ Configure ${pkgname} by adding .conf files into /etc/${pkgname}.d/" | ||||||
|  |     echo "┃ and maybe copy+edit one of the following to /etc/systemd/system/:" | ||||||
|  |     echo "┣━♦ /usr/lib/systemd/system/${pkgname}.service   (standard)" | ||||||
|  |     echo "┣━♦ /usr/lib/systemd/system/prisonparty.service (chroot)" | ||||||
|  |     echo "┗━━━━━━━━━━━━━━━──-" | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								contrib/package/arch/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								contrib/package/arch/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | ## import all *.conf files from the current folder (/etc/copyparty.d) | ||||||
|  | % ./ | ||||||
|  |  | ||||||
|  | # add additional .conf files to this folder; | ||||||
|  | # see example config files for reference: | ||||||
|  | # https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf | ||||||
|  | # https://github.com/9001/copyparty/tree/hovudstraum/docs/copyparty.d | ||||||
							
								
								
									
										32
									
								
								contrib/package/arch/copyparty.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								contrib/package/arch/copyparty.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | # this will start `/usr/bin/copyparty-sfx.py` | ||||||
|  | # and read config from `/etc/copyparty.d/*.conf` | ||||||
|  | # | ||||||
|  | # you probably want to: | ||||||
|  | #   change "User=cpp" and "/home/cpp/" to another user | ||||||
|  | # | ||||||
|  | # unless you add -q to disable logging, you may want to remove the | ||||||
|  | #   following line to allow buffering (slightly better performance): | ||||||
|  | #   Environment=PYTHONUNBUFFERED=x | ||||||
|  |  | ||||||
|  | [Unit] | ||||||
|  | Description=copyparty file server | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | Type=notify | ||||||
|  | SyslogIdentifier=copyparty | ||||||
|  | Environment=PYTHONUNBUFFERED=x | ||||||
|  | WorkingDirectory=/var/lib/copyparty-jail | ||||||
|  | ExecReload=/bin/kill -s USR1 $MAINPID | ||||||
|  |  | ||||||
|  | # user to run as + where the TLS certificate is (if any) | ||||||
|  | User=cpp | ||||||
|  | Environment=XDG_CONFIG_HOME=/home/cpp/.config | ||||||
|  |  | ||||||
|  | # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running | ||||||
|  | ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' | ||||||
|  |  | ||||||
|  | # run copyparty | ||||||
|  | ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
							
								
								
									
										3
									
								
								contrib/package/arch/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contrib/package/arch/index.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | this is `/var/lib/copyparty-jail`, the fallback webroot when copyparty has not yet been configured | ||||||
|  |  | ||||||
|  | please add some `*.conf` files to `/etc/copyparty.d/` | ||||||
							
								
								
									
										31
									
								
								contrib/package/arch/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								contrib/package/arch/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | # this will start `/usr/bin/copyparty-sfx.py` | ||||||
|  | # in a chroot, preventing accidental access elsewhere | ||||||
|  | # and read config from `/etc/copyparty.d/*.conf` | ||||||
|  | # | ||||||
|  | # expose additional filesystem locations to copyparty | ||||||
|  | #   by listing them between the last `1000` and `--` | ||||||
|  | # | ||||||
|  | # `1000 1000` = what user to run copyparty as | ||||||
|  | # | ||||||
|  | # unless you add -q to disable logging, you may want to remove the | ||||||
|  | #   following line to allow buffering (slightly better performance): | ||||||
|  | #   Environment=PYTHONUNBUFFERED=x | ||||||
|  |  | ||||||
|  | [Unit] | ||||||
|  | Description=copyparty file server | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | SyslogIdentifier=prisonparty | ||||||
|  | Environment=PYTHONUNBUFFERED=x | ||||||
|  | WorkingDirectory=/var/lib/copyparty-jail | ||||||
|  | ExecReload=/bin/kill -s USR1 $MAINPID | ||||||
|  |  | ||||||
|  | # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running | ||||||
|  | ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' | ||||||
|  |  | ||||||
|  | # run copyparty | ||||||
|  | ExecStart=/bin/bash /usr/bin/prisonparty /var/lib/copyparty-jail 1000 1000 /etc/copyparty.d -- \ | ||||||
|  |   /usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
							
								
								
									
										59
									
								
								contrib/package/nix/copyparty/default.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								contrib/package/nix/copyparty/default.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | { lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen, | ||||||
|  |  | ||||||
|  | # use argon2id-hashed passwords in config files (sha2 is always available) | ||||||
|  | withHashedPasswords ? true, | ||||||
|  |  | ||||||
|  | # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing | ||||||
|  | withThumbnails ? true, | ||||||
|  |  | ||||||
|  | # create thumbnails with PyVIPS; even faster, uses more memory | ||||||
|  | # -- can be combined with Pillow to support more filetypes | ||||||
|  | withFastThumbnails ? false, | ||||||
|  |  | ||||||
|  | # enable FFmpeg; thumbnails for most filetypes (also video and audio), extract audio metadata, transcode audio to opus | ||||||
|  | # -- possibly dangerous if you allow anonymous uploads, since FFmpeg has a huge attack surface | ||||||
|  | # -- can be combined with Thumbnails and/or FastThumbnails, since FFmpeg is slower than both | ||||||
|  | withMediaProcessing ? true, | ||||||
|  |  | ||||||
|  | # if MediaProcessing is not enabled, you probably want this instead (less accurate, but much safer and faster) | ||||||
|  | withBasicAudioMetadata ? false, | ||||||
|  |  | ||||||
|  | # enable FTPS support in the FTP server | ||||||
|  | withFTPS ? false, | ||||||
|  |  | ||||||
|  | # samba/cifs server; dangerous and buggy, enable if you really need it | ||||||
|  | withSMB ? false, | ||||||
|  |  | ||||||
|  | }: | ||||||
|  |  | ||||||
|  | let | ||||||
|  |   pinData = lib.importJSON ./pin.json; | ||||||
|  |   pyEnv = python.withPackages (ps: | ||||||
|  |     with ps; [ | ||||||
|  |       jinja2 | ||||||
|  |     ] | ||||||
|  |     ++ lib.optional withSMB impacket | ||||||
|  |     ++ lib.optional withFTPS pyopenssl | ||||||
|  |     ++ lib.optional withThumbnails pillow | ||||||
|  |     ++ lib.optional withFastThumbnails pyvips | ||||||
|  |     ++ lib.optional withMediaProcessing ffmpeg | ||||||
|  |     ++ lib.optional withBasicAudioMetadata mutagen | ||||||
|  |     ++ lib.optional withHashedPasswords argon2-cffi | ||||||
|  |     ); | ||||||
|  | in stdenv.mkDerivation { | ||||||
|  |   pname = "copyparty"; | ||||||
|  |   version = pinData.version; | ||||||
|  |   src = fetchurl { | ||||||
|  |     url = pinData.url; | ||||||
|  |     hash = pinData.hash; | ||||||
|  |   }; | ||||||
|  |   buildInputs = [ makeWrapper ]; | ||||||
|  |   dontUnpack = true; | ||||||
|  |   dontBuild = true; | ||||||
|  |   installPhase = '' | ||||||
|  |     install -Dm755 $src $out/share/copyparty-sfx.py | ||||||
|  |     makeWrapper ${pyEnv.interpreter} $out/bin/copyparty \ | ||||||
|  |       --set PATH '${lib.makeBinPath ([ utillinux ] ++ lib.optional withMediaProcessing ffmpeg)}:$PATH' \ | ||||||
|  |       --add-flags "$out/share/copyparty-sfx.py" | ||||||
|  |   ''; | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								contrib/package/nix/copyparty/pin.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								contrib/package/nix/copyparty/pin.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | { | ||||||
|  |     "url": "https://github.com/9001/copyparty/releases/download/v1.9.5/copyparty-sfx.py", | ||||||
|  |     "version": "1.9.5", | ||||||
|  |     "hash": "sha256-NT8se/f9vf6iiMVIOxTKdG7jddmVUCv12C0R2yhhy1Q=" | ||||||
|  | } | ||||||
							
								
								
									
										77
									
								
								contrib/package/nix/copyparty/update.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										77
									
								
								contrib/package/nix/copyparty/update.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
|  | # Update the Nix package pin | ||||||
|  | # | ||||||
|  | # Usage: ./update.sh [PATH] | ||||||
|  | # When the [PATH] is not set, it will fetch the latest release from the repo. | ||||||
|  | # With [PATH] set, it will hash the given file and generate the URL, | ||||||
|  | # base on the version contained within the file | ||||||
|  |  | ||||||
|  | import base64 | ||||||
|  | import json | ||||||
|  | import hashlib | ||||||
|  | import sys | ||||||
|  | import re | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | OUTPUT_FILE = Path("pin.json") | ||||||
|  | TARGET_ASSET = "copyparty-sfx.py" | ||||||
|  | HASH_TYPE = "sha256" | ||||||
|  | LATEST_RELEASE_URL = "https://api.github.com/repos/9001/copyparty/releases/latest" | ||||||
|  | DOWNLOAD_URL = lambda version: f"https://github.com/9001/copyparty/releases/download/v{version}/{TARGET_ASSET}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_formatted_hash(binary): | ||||||
|  |     hasher = hashlib.new("sha256") | ||||||
|  |     hasher.update(binary) | ||||||
|  |     asset_hash = hasher.digest() | ||||||
|  |     encoded_hash = base64.b64encode(asset_hash).decode("ascii") | ||||||
|  |     return f"{HASH_TYPE}-{encoded_hash}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def version_from_sfx(binary): | ||||||
|  |     result = re.search(b'^VER = "(.*)"$', binary, re.MULTILINE) | ||||||
|  |     if result: | ||||||
|  |         return result.groups(1)[0].decode("ascii") | ||||||
|  |  | ||||||
|  |     raise ValueError("version not found in provided file") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def remote_release_pin(): | ||||||
|  |     import requests | ||||||
|  |  | ||||||
|  |     response = requests.get(LATEST_RELEASE_URL).json() | ||||||
|  |     version = response["tag_name"].lstrip("v") | ||||||
|  |     asset_info = [a for a in response["assets"] if a["name"] == TARGET_ASSET][0] | ||||||
|  |     download_url = asset_info["browser_download_url"] | ||||||
|  |     asset = requests.get(download_url) | ||||||
|  |     formatted_hash = get_formatted_hash(asset.content) | ||||||
|  |  | ||||||
|  |     result = {"url": download_url, "version": version, "hash": formatted_hash} | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def local_release_pin(path): | ||||||
|  |     asset = path.read_bytes() | ||||||
|  |     version = version_from_sfx(asset) | ||||||
|  |     download_url = DOWNLOAD_URL(version) | ||||||
|  |     formatted_hash = get_formatted_hash(asset) | ||||||
|  |  | ||||||
|  |     result = {"url": download_url, "version": version, "hash": formatted_hash} | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     if len(sys.argv) > 1: | ||||||
|  |         asset_path = Path(sys.argv[1]) | ||||||
|  |         result = local_release_pin(asset_path) | ||||||
|  |     else: | ||||||
|  |         result = remote_release_pin() | ||||||
|  |  | ||||||
|  |     print(result) | ||||||
|  |     json_result = json.dumps(result, indent=4) | ||||||
|  |     OUTPUT_FILE.write_text(json_result) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
							
								
								
									
										33
									
								
								contrib/plugins/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								contrib/plugins/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | # example resource files | ||||||
|  |  | ||||||
|  | can be provided to copyparty to tweak things | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## example `.epilogue.html` | ||||||
|  | save one of these as `.epilogue.html` inside a folder to customize it: | ||||||
|  |  | ||||||
|  | * [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## example browser-js | ||||||
|  | point `--js-browser` to one of these by URL: | ||||||
|  |  | ||||||
|  | * [`minimal-up2k.js`](minimal-up2k.js) is similar to the above `minimal-up2k.html` except it applies globally to all write-only folders | ||||||
|  | * [`up2k-hooks.js`](up2k-hooks.js) lets you specify a ruleset for files to skip uploading | ||||||
|  |   * [`up2k-hook-ytid.js`](up2k-hook-ytid.js) is a more specific example checking youtube-IDs against some API | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## example browser-css | ||||||
|  | point `--css-browser` to one of these by URL: | ||||||
|  |  | ||||||
|  | * [`browser-icons.css`](browser-icons.css) adds filetype icons | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## meadup.js | ||||||
|  |  | ||||||
|  | * turns copyparty into chromecast just more flexible (and probably way more buggy) | ||||||
|  | * usage: put the js somewhere in the webroot and `--js-browser /memes/meadup.js` | ||||||
							
								
								
									
										71
									
								
								contrib/plugins/browser-icons.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								contrib/plugins/browser-icons.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | /* video, alternative 1: | ||||||
|  |    top-left icon, just like the other formats | ||||||
|  | ======================================================================= | ||||||
|  |  | ||||||
|  | #ggrid>a:is( | ||||||
|  | [href$=".mkv"i], | ||||||
|  | [href$=".mp4"i], | ||||||
|  | [href$=".webm"i], | ||||||
|  | ):before { | ||||||
|  |     content: '📺'; | ||||||
|  | } | ||||||
|  | */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* video, alternative 2: | ||||||
|  |    play-icon in the middle of the thumbnail | ||||||
|  | ======================================================================= | ||||||
|  | */ | ||||||
|  | #ggrid>a:is( | ||||||
|  | [href$=".mkv"i], | ||||||
|  | [href$=".mp4"i], | ||||||
|  | [href$=".webm"i], | ||||||
|  | ) { | ||||||
|  | 	position: relative; | ||||||
|  | 	overflow: hidden; | ||||||
|  | } | ||||||
|  | #ggrid>a:is( | ||||||
|  | [href$=".mkv"i], | ||||||
|  | [href$=".mp4"i], | ||||||
|  | [href$=".webm"i], | ||||||
|  | ):before { | ||||||
|  |     content: '▶'; | ||||||
|  | 	opacity: .8; | ||||||
|  | 	margin: 0; | ||||||
|  | 	padding: 1em .5em 1em .7em; | ||||||
|  | 	border-radius: 9em; | ||||||
|  | 	line-height: 0; | ||||||
|  | 	color: #fff; | ||||||
|  | 	text-shadow: none; | ||||||
|  | 	background: rgba(0, 0, 0, 0.7); | ||||||
|  | 	left: calc(50% - 1em); | ||||||
|  | 	top: calc(50% - 1.4em); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* audio */ | ||||||
|  | #ggrid>a:is( | ||||||
|  | [href$=".mp3"i], | ||||||
|  | [href$=".ogg"i], | ||||||
|  | [href$=".opus"i], | ||||||
|  | [href$=".flac"i], | ||||||
|  | [href$=".m4a"i], | ||||||
|  | [href$=".aac"i], | ||||||
|  | ):before { | ||||||
|  |     content: '🎵'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* image */ | ||||||
|  | #ggrid>a:is( | ||||||
|  | [href$=".jpg"i], | ||||||
|  | [href$=".jpeg"i], | ||||||
|  | [href$=".png"i], | ||||||
|  | [href$=".gif"i], | ||||||
|  | [href$=".webp"i], | ||||||
|  | ):before { | ||||||
|  |     content: '🎨'; | ||||||
|  | } | ||||||
							
								
								
									
										506
									
								
								contrib/plugins/meadup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								contrib/plugins/meadup.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,506 @@ | |||||||
|  | // USAGE: | ||||||
|  | //   place this file somewhere in the webroot and then | ||||||
|  | //   python3 -m copyparty --js-browser /memes/meadup.js | ||||||
|  | // | ||||||
|  | // FEATURES: | ||||||
|  | // * adds an onscreen keyboard for operating a media center remotely, | ||||||
|  | //    relies on https://github.com/9001/copyparty/blob/hovudstraum/bin/mtag/very-bad-idea.py | ||||||
|  | // * adds an interactive anime girl (if you can find the dependencies) | ||||||
|  |  | ||||||
|  | var hambagas = [ | ||||||
|  |     "https://www.youtube.com/watch?v=pFA3KGp4GuU" | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | // keybaord, | ||||||
|  | //   onscreen keyboard by @steinuil | ||||||
|  | function initKeybaord(BASE_URL, HAMBAGA, consoleLog, consoleError) { | ||||||
|  |     document.querySelector('.keybaord-container').innerHTML = ` | ||||||
|  |       <div class="keybaord-body"> | ||||||
|  |         <div class="keybaord-row keybaord-row-1"> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Escape"> | ||||||
|  |             esc | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F1"> | ||||||
|  |             F1 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F2"> | ||||||
|  |             F2 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F3"> | ||||||
|  |             F3 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F4"> | ||||||
|  |             F4 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F5"> | ||||||
|  |             F5 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F6"> | ||||||
|  |             F6 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F7"> | ||||||
|  |             F7 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F8"> | ||||||
|  |             F8 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F9"> | ||||||
|  |             F9 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F10"> | ||||||
|  |             F10 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F11"> | ||||||
|  |             F11 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="F12"> | ||||||
|  |             F12 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Insert"> | ||||||
|  |             ins | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Delete"> | ||||||
|  |             del | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row keybaord-row-2"> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="\`"> | ||||||
|  |             \` | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="1"> | ||||||
|  |             1 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="2"> | ||||||
|  |             2 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="3"> | ||||||
|  |             3 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="4"> | ||||||
|  |             4 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="5"> | ||||||
|  |             5 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="6"> | ||||||
|  |             6 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="7"> | ||||||
|  |             7 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="8"> | ||||||
|  |             8 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="9"> | ||||||
|  |             9 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="0"> | ||||||
|  |             0 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="-"> | ||||||
|  |             - | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="="> | ||||||
|  |             = | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-backspace" data-keybaord-key="BackSpace"> | ||||||
|  |             backspace | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row keybaord-row-3"> | ||||||
|  |           <div class="keybaord-key keybaord-tab" data-keybaord-key="Tab"> | ||||||
|  |             tab | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="q"> | ||||||
|  |             q | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="w"> | ||||||
|  |             w | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="e"> | ||||||
|  |             e | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="r"> | ||||||
|  |             r | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="t"> | ||||||
|  |             t | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="y"> | ||||||
|  |             y | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="u"> | ||||||
|  |             u | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="i"> | ||||||
|  |             i | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="o"> | ||||||
|  |             o | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="p"> | ||||||
|  |             p | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="["> | ||||||
|  |             [ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="]"> | ||||||
|  |             ] | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-enter" data-keybaord-key="Return"> | ||||||
|  |             enter | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row keybaord-row-4"> | ||||||
|  |           <div class="keybaord-key keybaord-capslock" data-keybaord-key="HAMBAGA"> | ||||||
|  |             🍔 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="a"> | ||||||
|  |             a | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="s"> | ||||||
|  |             s | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="d"> | ||||||
|  |             d | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="f"> | ||||||
|  |             f | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="g"> | ||||||
|  |             g | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="h"> | ||||||
|  |             h | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="j"> | ||||||
|  |             j | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="k"> | ||||||
|  |             k | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="l"> | ||||||
|  |             l | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key=";"> | ||||||
|  |             ; | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="'"> | ||||||
|  |             ' | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-backslash" data-keybaord-key="\\"> | ||||||
|  |             \\ | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row keybaord-row-5"> | ||||||
|  |           <div class="keybaord-key keybaord-lshift" data-keybaord-key="Shift_L"> | ||||||
|  |             shift | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="\\"> | ||||||
|  |             \\ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="z"> | ||||||
|  |             z | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="x"> | ||||||
|  |             x | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="c"> | ||||||
|  |             c | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="v"> | ||||||
|  |             v | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="b"> | ||||||
|  |             b | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="n"> | ||||||
|  |             n | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="m"> | ||||||
|  |             m | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key=","> | ||||||
|  |             , | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="."> | ||||||
|  |             . | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="/"> | ||||||
|  |             / | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-rshift" data-keybaord-key="Shift_R"> | ||||||
|  |             shift | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row keybaord-row-6"> | ||||||
|  |           <div class="keybaord-key keybaord-lctrl" data-keybaord-key="Control_L"> | ||||||
|  |             ctrl | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-super" data-keybaord-key="Meta_L"> | ||||||
|  |             win | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-alt" data-keybaord-key="Alt_L"> | ||||||
|  |             alt | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-spacebar" data-keybaord-key="space"> | ||||||
|  |             space | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-altgr" data-keybaord-key="Alt_R"> | ||||||
|  |             altgr | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-what" data-keybaord-key="Menu"> | ||||||
|  |             menu | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key keybaord-rctrl" data-keybaord-key="Control_R"> | ||||||
|  |             ctrl | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="keybaord-row"> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="XF86AudioLowerVolume"> | ||||||
|  |             🔉 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="XF86AudioRaiseVolume"> | ||||||
|  |             🔊 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Left"> | ||||||
|  |             ⬅️ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Down"> | ||||||
|  |             ⬇️ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Up"> | ||||||
|  |             ⬆️ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Right"> | ||||||
|  |             ➡️ | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Page_Up"> | ||||||
|  |             PgUp | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Page_Down"> | ||||||
|  |             PgDn | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="Home"> | ||||||
|  |             🏠 | ||||||
|  |           </div> | ||||||
|  |           <div class="keybaord-key" data-keybaord-key="End"> | ||||||
|  |             End | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       <div> | ||||||
|  |     `; | ||||||
|  |  | ||||||
|  |     function arraySample(array) { | ||||||
|  |         return array[Math.floor(Math.random() * array.length)]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function sendMessage(msg) { | ||||||
|  |         return fetch(BASE_URL, { | ||||||
|  |             method: "POST", | ||||||
|  |             headers: { | ||||||
|  |                 "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", | ||||||
|  |             }, | ||||||
|  |             body: "msg=" + encodeURIComponent(msg), | ||||||
|  |         }).then( | ||||||
|  |             (r) => r.text(), // so the response body shows up in network tab | ||||||
|  |             (err) => consoleError(err) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     const MODIFIER_ON_CLASS = "keybaord-modifier-on"; | ||||||
|  |     const KEY_DATASET = "data-keybaord-key"; | ||||||
|  |     const KEY_CLASS = "keybaord-key"; | ||||||
|  |  | ||||||
|  |     const modifiers = new Set() | ||||||
|  |  | ||||||
|  |     function toggleModifier(button, key) { | ||||||
|  |         button.classList.toggle(MODIFIER_ON_CLASS); | ||||||
|  |         if (modifiers.has(key)) { | ||||||
|  |             modifiers.delete(key); | ||||||
|  |         } else { | ||||||
|  |             modifiers.add(key); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function popModifiers() { | ||||||
|  |         let modifierString = ""; | ||||||
|  |  | ||||||
|  |         modifiers.forEach((mod) => { | ||||||
|  |             document.querySelector("[" + KEY_DATASET + "='" + mod + "']") | ||||||
|  |                 .classList.remove(MODIFIER_ON_CLASS); | ||||||
|  |  | ||||||
|  |             modifierString += mod + "+"; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         modifiers.clear(); | ||||||
|  |  | ||||||
|  |         return modifierString; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Array.from(document.querySelectorAll("." + KEY_CLASS)).forEach((button) => { | ||||||
|  |         const key = button.dataset.keybaordKey; | ||||||
|  |  | ||||||
|  |         button.addEventListener("click", (ev) => { | ||||||
|  |             switch (key) { | ||||||
|  |                 case "HAMBAGA": | ||||||
|  |                     sendMessage(arraySample(HAMBAGA)); | ||||||
|  |                     break; | ||||||
|  |  | ||||||
|  |                 case "Shift_L": | ||||||
|  |                 case "Shift_R": | ||||||
|  |  | ||||||
|  |                 case "Control_L": | ||||||
|  |                 case "Control_R": | ||||||
|  |  | ||||||
|  |                 case "Meta_L": | ||||||
|  |  | ||||||
|  |                 case "Alt_L": | ||||||
|  |                 case "Alt_R": | ||||||
|  |                     toggleModifier(button, key); | ||||||
|  |                     break; | ||||||
|  |  | ||||||
|  |                 default: { | ||||||
|  |                     const keyWithModifiers = popModifiers() + key; | ||||||
|  |  | ||||||
|  |                     consoleLog(keyWithModifiers); | ||||||
|  |  | ||||||
|  |                     sendMessage("key " + keyWithModifiers) | ||||||
|  |                         .then(() => consoleLog(keyWithModifiers + " OK")); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // keybaord integration | ||||||
|  | (function () { | ||||||
|  |     var o = mknod('div'); | ||||||
|  |     clmod(o, 'keybaord-container', 1); | ||||||
|  |     ebi('op_msg').appendChild(o); | ||||||
|  |  | ||||||
|  |     o = mknod('style'); | ||||||
|  |     o.innerHTML = ` | ||||||
|  | .keybaord-body { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-flow: column nowrap; | ||||||
|  |     margin: .6em 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-row { | ||||||
|  | 	display: flex; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key { | ||||||
|  | 	border: 1px solid rgba(128,128,128,0.2); | ||||||
|  | 	width: 41px; | ||||||
|  | 	height: 40px; | ||||||
|  |  | ||||||
|  | 	display: flex; | ||||||
|  | 	justify-content: center; | ||||||
|  | 	align-items: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key:active { | ||||||
|  | 	background-color: lightgrey; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-modifier-on { | ||||||
|  | 	background-color: lightblue; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-backspace { | ||||||
|  | 	width: 82px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-tab { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-enter { | ||||||
|  | 	width: 69px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-capslock { | ||||||
|  | 	width: 80px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-backslash { | ||||||
|  | 	width: 88px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-lshift { | ||||||
|  | 	width: 65px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-rshift { | ||||||
|  | 	width: 103px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-lctrl { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-super { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-alt { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-altgr { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-what { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-rctrl { | ||||||
|  | 	width: 55px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .keybaord-key.keybaord-spacebar { | ||||||
|  | 	width: 302px; | ||||||
|  | } | ||||||
|  | `; | ||||||
|  |     document.head.appendChild(o); | ||||||
|  |  | ||||||
|  |     initKeybaord('/', hambagas, | ||||||
|  |         (msg) => { toast.inf(2, msg.toString()) }, | ||||||
|  |         (msg) => { toast.err(30, msg.toString()) }); | ||||||
|  | })(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // live2d (dumb pointless meme) | ||||||
|  | //   dependencies for this part are not tracked in git | ||||||
|  | //   so delete this section if you wanna use this file | ||||||
|  | //   (or supply your own l2d model and js) | ||||||
|  | (function () { | ||||||
|  |     var o = mknod('link'); | ||||||
|  |     o.setAttribute('rel', 'stylesheet'); | ||||||
|  |     o.setAttribute('href', "/bad-memes/pio.css"); | ||||||
|  |     document.head.appendChild(o); | ||||||
|  |  | ||||||
|  |     o = mknod('style'); | ||||||
|  |     o.innerHTML = '.pio-container{text-shadow:none;z-index:1}'; | ||||||
|  |     document.head.appendChild(o); | ||||||
|  |  | ||||||
|  |     o = mknod('div'); | ||||||
|  |     clmod(o, 'pio-container', 1); | ||||||
|  |     o.innerHTML = '<div class="pio-action"></div><canvas id="pio" width="280" height="500"></canvas>'; | ||||||
|  |     document.body.appendChild(o); | ||||||
|  |  | ||||||
|  |     var remaining = 3; | ||||||
|  |     for (var a of ['pio', 'l2d', 'fireworks']) { | ||||||
|  |         import_js(`/bad-memes/${a}.js`, function () { | ||||||
|  |             if (remaining --> 1) | ||||||
|  |                 return; | ||||||
|  |  | ||||||
|  |             o = mknod('script'); | ||||||
|  |             o.innerHTML = 'var pio = new Paul_Pio({"selector":[],"mode":"fixed","hidden":false,"content":{"close":"ok bye"},"model":["/bad-memes/sagiri/model.json"]});'; | ||||||
|  |             document.body.appendChild(o); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | })(); | ||||||
							
								
								
									
										46
									
								
								contrib/plugins/minimal-up2k.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								contrib/plugins/minimal-up2k.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | <!-- | ||||||
|  |   NOTE: DEPRECATED; please use the javascript version instead: | ||||||
|  |   https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/minimal-up2k.js | ||||||
|  |  | ||||||
|  |   ---- | ||||||
|  |  | ||||||
|  |   save this as .epilogue.html inside a write-only folder to declutter the UI,  makes it look like | ||||||
|  |   https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png | ||||||
|  |  | ||||||
|  |   only works if you disable the prologue/epilogue sandbox with --no-sb-lg | ||||||
|  |   which should probably be combined with --no-dot-ren to prevent damage | ||||||
|  |   (`no_sb_lg` can also be set per-volume with volflags) | ||||||
|  | --> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  |  | ||||||
|  |     /* make the up2k ui REALLY minimal by hiding a bunch of stuff: */ | ||||||
|  |  | ||||||
|  |     #ops, #tree, #path, #wfp,  /* main tabs and navigators (tree/breadcrumbs) */ | ||||||
|  |  | ||||||
|  |     #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw),  /* most of the config options */ | ||||||
|  |  | ||||||
|  |     #srch_dz, #srch_zd,  /* the filesearch dropzone */ | ||||||
|  |  | ||||||
|  |     #u2cards, #u2etaw  /* and the upload progress tabs */ | ||||||
|  |  | ||||||
|  |     {display: none !important}  /* do it! */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /* add some margins because now it's weird */ | ||||||
|  |     .opview {margin-top: 2.5em} | ||||||
|  |     #op_up2k {margin-top: 6em} | ||||||
|  |  | ||||||
|  |     /* and embiggen the upload button */ | ||||||
|  |     #u2conf #u2btn, #u2btn {padding:1.5em 0} | ||||||
|  |  | ||||||
|  |     /* adjust the button area a bit */ | ||||||
|  |     #u2conf.w, #u2conf.ww {width: 35em !important; margin: 5em auto} | ||||||
|  |  | ||||||
|  |     /* a */ | ||||||
|  |     #op_up2k {min-height: 0} | ||||||
|  |  | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | <a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a> | ||||||
							
								
								
									
										59
									
								
								contrib/plugins/minimal-up2k.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								contrib/plugins/minimal-up2k.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | /* | ||||||
|  |  | ||||||
|  | makes the up2k ui REALLY minimal by hiding a bunch of stuff | ||||||
|  |  | ||||||
|  | almost the same as minimal-up2k.html except this one...: | ||||||
|  |  | ||||||
|  |  -- applies to every write-only folder when used with --js-browser | ||||||
|  |  | ||||||
|  |  -- only applies if javascript is enabled | ||||||
|  |  | ||||||
|  |  -- doesn't hide the total upload ETA display | ||||||
|  |  | ||||||
|  |  -- looks slightly better | ||||||
|  |  | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | var u2min = ` | ||||||
|  | <style> | ||||||
|  |  | ||||||
|  | #ops, #path, #tree, #files, #wfp, | ||||||
|  | #u2conf td.c+.c, #u2cards, #srch_dz, #srch_zd { | ||||||
|  |   display: none !important; | ||||||
|  | } | ||||||
|  | #u2conf {margin:5em auto 0 auto !important} | ||||||
|  | #u2conf.ww {width:70em} | ||||||
|  | #u2conf.w {width:50em} | ||||||
|  | #u2conf.w .c, | ||||||
|  | #u2conf.w #u2btn_cw {text-align:left} | ||||||
|  | #u2conf.w #u2btn_cw {width:70%} | ||||||
|  | #u2etaw {margin:3em auto} | ||||||
|  | #u2etaw.w { | ||||||
|  |   text-align: center; | ||||||
|  |   margin: -3.5em auto 5em auto; | ||||||
|  | } | ||||||
|  | #u2etaw.w #u2etas {margin-right:-37em} | ||||||
|  | #u2etaw.w #u2etas.o {margin-top:-2.2em} | ||||||
|  | #u2etaw.ww {margin:-1em auto} | ||||||
|  | #u2etaw.ww #u2etas {padding-left:4em} | ||||||
|  | #u2etas { | ||||||
|  |   background: none !important; | ||||||
|  |   border: none !important; | ||||||
|  | } | ||||||
|  | #wrap {margin-left:2em !important} | ||||||
|  | .logue { | ||||||
|  |   border: none !important; | ||||||
|  |   margin: 2em auto !important; | ||||||
|  | } | ||||||
|  | .logue:before {content:'' !important} | ||||||
|  |  | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | <a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a> | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | if (!has(perms, 'read')) { | ||||||
|  |   var e2 = mknod('div'); | ||||||
|  |   e2.innerHTML = u2min; | ||||||
|  |   ebi('wrap').insertBefore(e2, QS('#wfp')); | ||||||
|  | } | ||||||
							
								
								
									
										208
									
								
								contrib/plugins/rave.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								contrib/plugins/rave.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | |||||||
|  | /* untz untz untz untz */ | ||||||
|  |  | ||||||
|  | (function () { | ||||||
|  |  | ||||||
|  |     var can, ctx, W, H, fft, buf, bars, barw, pv, | ||||||
|  |         hue = 0, | ||||||
|  |         ibeat = 0, | ||||||
|  |         beats = [9001], | ||||||
|  |         beats_url = '', | ||||||
|  |         uofs = 0, | ||||||
|  |         ops = ebi('ops'), | ||||||
|  |         raving = false, | ||||||
|  |         recalc = 0, | ||||||
|  |         cdown = 0, | ||||||
|  |         FC = 0.9, | ||||||
|  |         css = `<style> | ||||||
|  |  | ||||||
|  | #fft { | ||||||
|  |     position: fixed; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     z-index: -1; | ||||||
|  | } | ||||||
|  | body { | ||||||
|  |     box-shadow: inset 0 0 0 white; | ||||||
|  | } | ||||||
|  | #ops>a, | ||||||
|  | #path>a { | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
|  | /* | ||||||
|  | body.untz { | ||||||
|  |     animation: untz-body 200ms ease-out; | ||||||
|  | } | ||||||
|  | @keyframes untz-body { | ||||||
|  | 	0% {inset 0 0 20em white} | ||||||
|  | 	100% {inset 0 0 0 white} | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | :root, html.a, html.b, html.c, html.d, html.e { | ||||||
|  |     --row-alt: rgba(48,52,78,0.2); | ||||||
|  | } | ||||||
|  | #files td { | ||||||
|  |     background: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </style>`; | ||||||
|  |  | ||||||
|  |     QS('body').appendChild(mknod('div', null, css)); | ||||||
|  |  | ||||||
|  |     function rave_load() { | ||||||
|  |         console.log('rave_load'); | ||||||
|  |         can = mknod('canvas', 'fft'); | ||||||
|  |         QS('body').appendChild(can); | ||||||
|  |         ctx = can.getContext('2d'); | ||||||
|  |  | ||||||
|  |         fft = new AnalyserNode(actx, { | ||||||
|  |             "fftSize": 2048, | ||||||
|  |             "maxDecibels": 0, | ||||||
|  |             "smoothingTimeConstant": 0.7, | ||||||
|  |         }); | ||||||
|  |         ibeat = 0; | ||||||
|  |         beats = [9001]; | ||||||
|  |         buf = new Uint8Array(fft.frequencyBinCount); | ||||||
|  |         bars = buf.length * FC; | ||||||
|  |         afilt.filters.push(fft); | ||||||
|  |         if (!raving) { | ||||||
|  |             raving = true; | ||||||
|  |             raver(); | ||||||
|  |         } | ||||||
|  |         beats_url = mp.au.src.split('?')[0].replace(/(.*\/)(.*)/, '$1.beats/$2.txt'); | ||||||
|  |         console.log("reading beats from", beats_url); | ||||||
|  |         var xhr = new XHR(); | ||||||
|  |         xhr.open('GET', beats_url, true); | ||||||
|  |         xhr.onload = readbeats; | ||||||
|  |         xhr.url = beats_url; | ||||||
|  |         xhr.send(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function rave_unload() { | ||||||
|  |         qsr('#fft'); | ||||||
|  |         can = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function readbeats() { | ||||||
|  |         if (this.url != beats_url) | ||||||
|  |             return console.log('old beats??', this.url, beats_url); | ||||||
|  |  | ||||||
|  |         var sbeats = this.responseText.replace(/\r/g, '').split(/\n/g); | ||||||
|  |         if (sbeats.length < 3) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         beats = []; | ||||||
|  |         for (var a = 0; a < sbeats.length; a++) | ||||||
|  |             beats.push(parseFloat(sbeats[a])); | ||||||
|  |  | ||||||
|  |         var end = beats.slice(-2), | ||||||
|  |             t = end[1], | ||||||
|  |             d = t - end[0]; | ||||||
|  |  | ||||||
|  |         while (d > 0.1 && t < 1200) | ||||||
|  |             beats.push(t += d); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function hrand() { | ||||||
|  |         return Math.random() - 0.5; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function raver() { | ||||||
|  |         if (!can) { | ||||||
|  |             raving = false; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         requestAnimationFrame(raver); | ||||||
|  |         if (!mp || !mp.au || mp.au.paused) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         if (--uofs >= 0) { | ||||||
|  |             document.body.style.marginLeft = hrand() * uofs + 'px'; | ||||||
|  |             ebi('tree').style.marginLeft = hrand() * uofs + 'px'; | ||||||
|  |             for (var a of QSA('#ops>a, #path>a, #pctl>a')) | ||||||
|  |                 a.style.transform = 'translate(' + hrand() * uofs * 1 + 'px, ' + hrand() * uofs * 0.7 + 'px) rotate(' + Math.random() * uofs * 0.7 + 'deg)' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (--recalc < 0) { | ||||||
|  |             recalc = 60; | ||||||
|  |             var tree = ebi('tree'), | ||||||
|  |                 x = tree.style.display == 'none' ? 0 : tree.offsetWidth; | ||||||
|  |  | ||||||
|  |             //W = can.width = window.innerWidth - x; | ||||||
|  |             //H = can.height = window.innerHeight; | ||||||
|  |             //H = ebi('widget').offsetTop; | ||||||
|  |             W = can.width = bars; | ||||||
|  |             H = can.height = 512; | ||||||
|  |             barw = 1; //parseInt(0.8 + W / bars); | ||||||
|  |             can.style.left = x + 'px'; | ||||||
|  |             can.style.width = (window.innerWidth - x) + 'px'; | ||||||
|  |             can.style.height = ebi('widget').offsetTop + 'px'; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         //if (--cdown == 1) | ||||||
|  |         //    clmod(ops, 'untz'); | ||||||
|  |  | ||||||
|  |         fft.getByteFrequencyData(buf); | ||||||
|  |  | ||||||
|  |         var imax = 0, vmax = 0; | ||||||
|  |         for (var a = 10; a < 50; a++) | ||||||
|  |             if (vmax < buf[a]) { | ||||||
|  |                 vmax = buf[a]; | ||||||
|  |                 imax = a; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         hue = hue * 0.93 + imax * 0.07; | ||||||
|  |  | ||||||
|  |         ctx.fillStyle = 'rgba(0,0,0,0)'; | ||||||
|  |         ctx.fillRect(0, 0, W, H); | ||||||
|  |         ctx.clearRect(0, 0, W, H); | ||||||
|  |         ctx.fillStyle = 'hsla(' + (hue * 2.5) + ',100%,50%,0.7)'; | ||||||
|  |  | ||||||
|  |         var x = 0, mul = (H / 256) * 0.5; | ||||||
|  |         for (var a = 0; a < buf.length * FC; a++) { | ||||||
|  |             var v = buf[a] * mul * (1 + 0.69 * a / buf.length); | ||||||
|  |             ctx.fillRect(x, H - v, barw, v); | ||||||
|  |             x += barw; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var t = mp.au.currentTime + 0.05; | ||||||
|  |  | ||||||
|  |         if (ibeat >= beats.length || beats[ibeat] > t) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         while (ibeat < beats.length && beats[ibeat++] < t) | ||||||
|  |             continue; | ||||||
|  |  | ||||||
|  |         return untz(); | ||||||
|  |  | ||||||
|  |         var cv = 0; | ||||||
|  |         for (var a = 0; a < 128; a++) | ||||||
|  |             cv += buf[a]; | ||||||
|  |  | ||||||
|  |         if (cv - pv > 1000) { | ||||||
|  |             console.log(pv, cv, cv - pv); | ||||||
|  |             if (cdown < 0) { | ||||||
|  |                 clmod(ops, 'untz', 1); | ||||||
|  |                 cdown = 20; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         pv = cv; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function untz() { | ||||||
|  |         console.log('untz'); | ||||||
|  |         uofs = 14; | ||||||
|  |         document.body.animate([ | ||||||
|  |             { boxShadow: 'inset 0 0 1em #f0c' }, | ||||||
|  |             { boxShadow: 'inset 0 0 20em #f0c', offset: 0.2 }, | ||||||
|  |             { boxShadow: 'inset 0 0 0 #f0c' }, | ||||||
|  |         ], { duration: 200, iterations: 1 }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     afilt.plugs.push({ | ||||||
|  |         "en": true, | ||||||
|  |         "load": rave_load, | ||||||
|  |         "unload": rave_unload | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  | })(); | ||||||
							
								
								
									
										297
									
								
								contrib/plugins/up2k-hook-ytid.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								contrib/plugins/up2k-hook-ytid.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,297 @@ | |||||||
|  | // way more specific example -- | ||||||
|  | // assumes all files dropped into the uploader have a youtube-id somewhere in the filename, | ||||||
|  | // locates the youtube-ids and passes them to an API which returns a list of IDs which should be uploaded | ||||||
|  | // | ||||||
|  | // also tries to find the youtube-id in the embedded metadata | ||||||
|  | // | ||||||
|  | // assumes copyparty is behind nginx as /ytq is a standalone service which must be rproxied in place | ||||||
|  |  | ||||||
|  | function up2k_namefilter(good_files, nil_files, bad_files, hooks) { | ||||||
|  |     var passthru = up2k.uc.fsearch; | ||||||
|  |     if (passthru) | ||||||
|  |         return hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); | ||||||
|  |  | ||||||
|  |     a_up2k_namefilter(good_files, nil_files, bad_files, hooks).then(() => { }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ebi('op_up2k').appendChild(mknod('input','unick')); | ||||||
|  |  | ||||||
|  | function bstrpos(buf, ptn) { | ||||||
|  |     var ofs = 0, | ||||||
|  |         ch0 = ptn[0], | ||||||
|  |         sz = buf.byteLength; | ||||||
|  |  | ||||||
|  |     while (true) { | ||||||
|  |         ofs = buf.indexOf(ch0, ofs); | ||||||
|  |         if (ofs < 0 || ofs >= sz) | ||||||
|  |             return -1; | ||||||
|  |  | ||||||
|  |         for (var a = 1; a < ptn.length; a++) | ||||||
|  |             if (buf[ofs + a] !== ptn[a]) | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |         if (a === ptn.length) | ||||||
|  |             return ofs; | ||||||
|  |  | ||||||
|  |         ++ofs; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function a_up2k_namefilter(good_files, nil_files, bad_files, hooks) { | ||||||
|  |     var t0 = Date.now(), | ||||||
|  |         yt_ids = new Set(), | ||||||
|  |         textdec = new TextDecoder('latin1'), | ||||||
|  |         md_ptn = new TextEncoder().encode('youtube.com/watch?v='), | ||||||
|  |         file_ids = [],  // all IDs found for each good_files | ||||||
|  |         md_only = [],  // `${id} ${fn}` where ID was only found in metadata | ||||||
|  |         mofs = 0, | ||||||
|  |         mnchk = 0, | ||||||
|  |         mfile = '', | ||||||
|  |         myid = localStorage.getItem('ytid_t0'); | ||||||
|  |  | ||||||
|  |     if (!myid) | ||||||
|  |         localStorage.setItem('ytid_t0', myid = Date.now()); | ||||||
|  |  | ||||||
|  |     for (var a = 0; a < good_files.length; a++) { | ||||||
|  |         var [fobj, name] = good_files[a], | ||||||
|  |             cname = name,  // will clobber | ||||||
|  |             sz = fobj.size, | ||||||
|  |             ids = [], | ||||||
|  |             fn_ids = [], | ||||||
|  |             md_ids = [], | ||||||
|  |             id_ok = false, | ||||||
|  |             m; | ||||||
|  |  | ||||||
|  |         // all IDs found in this file | ||||||
|  |         file_ids.push(ids); | ||||||
|  |  | ||||||
|  |         // look for ID in filename; reduce the | ||||||
|  |         // metadata-scan intensity if the id looks safe | ||||||
|  |         m = /[\[(-]([\w-]{11})[\])]?\.(?:mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name); | ||||||
|  |         id_ok = !!m; | ||||||
|  |  | ||||||
|  |         while (true) { | ||||||
|  |             // fuzzy catch-all; | ||||||
|  |             // some ytdl fork did %(title)-%(id).%(ext) ... | ||||||
|  |             m = /(?:^|[^\w])([\w-]{11})(?:$|[^\w-])/.exec(cname); | ||||||
|  |             if (!m) | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |             cname = cname.replace(m[1], ''); | ||||||
|  |             yt_ids.add(m[1]); | ||||||
|  |             fn_ids.unshift(m[1]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // look for IDs in video metadata, | ||||||
|  |         if (/\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name)) { | ||||||
|  |             toast.show('inf r', 0, `analyzing file ${a + 1} / ${good_files.length} :\n${name}\n\nhave analysed ${++mnchk} files in ${(Date.now() - t0) / 1000} seconds, ${humantime((good_files.length - (a + 1)) * (((Date.now() - t0) / 1000) / mnchk))} remaining,\n\nbiggest offset so far is ${mofs}, in this file:\n\n${mfile}`); | ||||||
|  |  | ||||||
|  |             // check first and last 128 MiB; | ||||||
|  |             // pWxOroN5WCo.mkv @  6edb98 (6.92M) | ||||||
|  |             // Nf-nN1wF5Xo.mp4 @ 4a98034 (74.6M) | ||||||
|  |             var chunksz = 1024 * 1024 * 2,  // byte | ||||||
|  |                 aspan = id_ok ? 128 : 512;  // MiB | ||||||
|  |  | ||||||
|  |             aspan = parseInt(Math.min(sz / 2, aspan * 1024 * 1024) / chunksz) * chunksz; | ||||||
|  |             if (!aspan) | ||||||
|  |                 aspan = Math.min(sz, chunksz); | ||||||
|  |  | ||||||
|  |             for (var side = 0; side < 2; side++) { | ||||||
|  |                 var ofs = side ? Math.max(0, sz - aspan) : 0, | ||||||
|  |                     nchunks = aspan / chunksz; | ||||||
|  |  | ||||||
|  |                 for (var chunk = 0; chunk < nchunks; chunk++) { | ||||||
|  |                     var bchunk = await fobj.slice(ofs, ofs + chunksz + 16).arrayBuffer(), | ||||||
|  |                         uchunk = new Uint8Array(bchunk, 0, bchunk.byteLength), | ||||||
|  |                         bofs = bstrpos(uchunk, md_ptn), | ||||||
|  |                         absofs = Math.min(ofs + bofs, (sz - ofs) + bofs), | ||||||
|  |                         txt = bofs < 0 ? '' : textdec.decode(uchunk.subarray(bofs)), | ||||||
|  |                         m; | ||||||
|  |  | ||||||
|  |                     //console.log(`side ${ side }, chunk ${ chunk }, ofs ${ ofs }, bchunk ${ bchunk.byteLength }, txt ${ txt.length }`); | ||||||
|  |                     while (true) { | ||||||
|  |                         // mkv/webm have [a-z] immediately after url | ||||||
|  |                         m = /(youtube\.com\/watch\?v=[\w-]{11})/.exec(txt); | ||||||
|  |                         if (!m) | ||||||
|  |                             break; | ||||||
|  |  | ||||||
|  |                         txt = txt.replace(m[1], ''); | ||||||
|  |                         m = m[1].slice(-11); | ||||||
|  |  | ||||||
|  |                         console.log(`found ${m} @${bofs}, ${name} `); | ||||||
|  |                         yt_ids.add(m); | ||||||
|  |                         if (!has(fn_ids, m) && !has(md_ids, m)) { | ||||||
|  |                             md_ids.push(m); | ||||||
|  |                             md_only.push(`${m} ${name}`); | ||||||
|  |                         } | ||||||
|  |                         else | ||||||
|  |                             // id appears several times; make it preferred | ||||||
|  |                             md_ids.unshift(m); | ||||||
|  |  | ||||||
|  |                         // bail after next iteration | ||||||
|  |                         chunk = nchunks - 1; | ||||||
|  |                         side = 9; | ||||||
|  |  | ||||||
|  |                         if (mofs < absofs) { | ||||||
|  |                             mofs = absofs; | ||||||
|  |                             mfile = name; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     ofs += chunksz; | ||||||
|  |                     if (ofs >= sz) | ||||||
|  |                         break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (var yi of md_ids) | ||||||
|  |             ids.push(yi); | ||||||
|  |  | ||||||
|  |         for (var yi of fn_ids) | ||||||
|  |             if (!has(ids, yi)) | ||||||
|  |                 ids.push(yi); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (md_only.length) | ||||||
|  |         console.log('recovered the following youtube-IDs by inspecting metadata:\n\n' + md_only.join('\n')); | ||||||
|  |     else if (yt_ids.size) | ||||||
|  |         console.log('did not discover any additional youtube-IDs by inspecting metadata; all the IDs also existed in the filenames'); | ||||||
|  |     else | ||||||
|  |         console.log('failed to find any youtube-IDs at all, sorry'); | ||||||
|  |  | ||||||
|  |     if (false) { | ||||||
|  |         var msg = `finished analysing ${mnchk} files in ${(Date.now() - t0) / 1000} seconds,\n\nbiggest offset was ${mofs} in this file:\n\n${mfile}`, | ||||||
|  |             mfun = function () { toast.ok(0, msg); }; | ||||||
|  |  | ||||||
|  |         mfun(); | ||||||
|  |         setTimeout(mfun, 200); | ||||||
|  |  | ||||||
|  |         return hooks[0]([], [], [], hooks.slice(1)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var el = ebi('unick'), unick = el ? el.value : ''; | ||||||
|  |     if (unick) { | ||||||
|  |         console.log(`sending uploader nickname [${unick}]`); | ||||||
|  |         fetch(document.location, { | ||||||
|  |             method: 'POST', | ||||||
|  |             headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, | ||||||
|  |             body: 'msg=' + encodeURIComponent(unick) | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     toast.inf(5, `running query for ${yt_ids.size} youtube-IDs...`); | ||||||
|  |  | ||||||
|  |     var xhr = new XHR(); | ||||||
|  |     xhr.open('POST', '/ytq', true); | ||||||
|  |     xhr.setRequestHeader('Content-Type', 'text/plain'); | ||||||
|  |     xhr.onload = xhr.onerror = function () { | ||||||
|  |         if (this.status != 200) | ||||||
|  |             return toast.err(0, `sorry, database query failed ;_;\n\nplease let us know so we can look at it, thx!!\n\nerror ${this.status}: ${(this.response && this.response.err) || this.responseText}`); | ||||||
|  |  | ||||||
|  |         process_id_list(this.responseText); | ||||||
|  |     }; | ||||||
|  |     xhr.send(Array.from(yt_ids).join('\n')); | ||||||
|  |  | ||||||
|  |     function process_id_list(txt) { | ||||||
|  |         var wanted_ids = new Set(txt.trim().split('\n')), | ||||||
|  |             name_id = {}, | ||||||
|  |             wanted_names = new Set(),  // basenames with a wanted ID -- not including relpath | ||||||
|  |             wanted_names_scoped = {},  // basenames with a wanted ID -> list of dirs to search under | ||||||
|  |             wanted_files = new Set();  // filedrops | ||||||
|  |  | ||||||
|  |         for (var a = 0; a < good_files.length; a++) { | ||||||
|  |             var name = good_files[a][1]; | ||||||
|  |             for (var b = 0; b < file_ids[a].length; b++) | ||||||
|  |                 if (wanted_ids.has(file_ids[a][b])) { | ||||||
|  |                     // let the next stage handle this to prevent dupes | ||||||
|  |                     //wanted_files.add(good_files[a]); | ||||||
|  |  | ||||||
|  |                     var m = /(.*)\.(mp4|webm|mkv|flv|opus|ogg|mp3|m4a|aac)$/i.exec(name); | ||||||
|  |                     if (!m) | ||||||
|  |                         continue; | ||||||
|  |  | ||||||
|  |                     var [rd, fn] = vsplit(m[1]); | ||||||
|  |  | ||||||
|  |                     if (fn in wanted_names_scoped) | ||||||
|  |                         wanted_names_scoped[fn].push(rd); | ||||||
|  |                     else | ||||||
|  |                         wanted_names_scoped[fn] = [rd]; | ||||||
|  |  | ||||||
|  |                     wanted_names.add(fn); | ||||||
|  |                     name_id[m[1]] = file_ids[a][b]; | ||||||
|  |  | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // add all files with the same basename as each explicitly wanted file | ||||||
|  |         // (infojson/chatlog/etc when ID was discovered from metadata) | ||||||
|  |         for (var a = 0; a < good_files.length; a++) { | ||||||
|  |             var [rd, name] = vsplit(good_files[a][1]); | ||||||
|  |             for (var b = 0; b < 3; b++) { | ||||||
|  |                 name = name.replace(/\.[^\.]+$/, ''); | ||||||
|  |                 if (!wanted_names.has(name)) | ||||||
|  |                     continue; | ||||||
|  |  | ||||||
|  |                 var vid_fp = false; | ||||||
|  |                 for (var c of wanted_names_scoped[name]) | ||||||
|  |                     if (rd.startsWith(c)) | ||||||
|  |                         vid_fp = c + name; | ||||||
|  |  | ||||||
|  |                 if (!vid_fp) | ||||||
|  |                     continue; | ||||||
|  |  | ||||||
|  |                 var subdir = name_id[vid_fp]; | ||||||
|  |                 subdir = `v${subdir.slice(0, 1)}/${subdir}-${myid}`; | ||||||
|  |                 var newpath = subdir + '/' + good_files[a][1].split(/\//g).pop(); | ||||||
|  |  | ||||||
|  |                 // check if this file is a dupe | ||||||
|  |                 for (var c of good_files) | ||||||
|  |                     if (c[1] == newpath) | ||||||
|  |                         newpath = null; | ||||||
|  |  | ||||||
|  |                 if (!newpath) | ||||||
|  |                     break; | ||||||
|  |  | ||||||
|  |                 good_files[a][1] = newpath; | ||||||
|  |                 wanted_files.add(good_files[a]); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         function upload_filtered() { | ||||||
|  |             if (!wanted_files.size) | ||||||
|  |                 return modal.alert('Good news -- turns out we already have all those.\n\nBut thank you for checking in!'); | ||||||
|  |  | ||||||
|  |             hooks[0](Array.from(wanted_files), nil_files, bad_files, hooks.slice(1)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         function upload_all() { | ||||||
|  |             hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var n_skip = good_files.length - wanted_files.size, | ||||||
|  |             msg = `you added ${good_files.length} files; ${good_files.length == n_skip ? 'all' : n_skip} of them were skipped --\neither because we already have them,\nor because there is no youtube-ID in your filenames.\n\n<code>OK</code> / <code>Enter</code> = continue uploading just the ${wanted_files.size} files we definitely need\n\n<code>Cancel</code> / <code>ESC</code> = override the filter; upload ALL the files you added`; | ||||||
|  |  | ||||||
|  |         if (!n_skip) | ||||||
|  |             upload_filtered(); | ||||||
|  |         else | ||||||
|  |             modal.confirm(msg, upload_filtered, upload_all); | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | up2k_hooks.push(function () { | ||||||
|  |     up2k.gotallfiles.unshift(up2k_namefilter); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // persist/restore nickname field if present | ||||||
|  | setInterval(function () { | ||||||
|  |     var o = ebi('unick'); | ||||||
|  |     if (!o || document.activeElement == o) | ||||||
|  |         return; | ||||||
|  |  | ||||||
|  |     o.oninput = function () { | ||||||
|  |         localStorage.setItem('unick', o.value); | ||||||
|  |     }; | ||||||
|  |     o.value = localStorage.getItem('unick') || ''; | ||||||
|  | }, 1000); | ||||||
							
								
								
									
										45
									
								
								contrib/plugins/up2k-hooks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								contrib/plugins/up2k-hooks.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | // hooks into up2k | ||||||
|  |  | ||||||
|  | function up2k_namefilter(good_files, nil_files, bad_files, hooks) { | ||||||
|  |     // is called when stuff is dropped into the browser, | ||||||
|  |     // after iterating through the directory tree and discovering all files, | ||||||
|  |     // before the upload confirmation dialogue is shown | ||||||
|  |  | ||||||
|  |     // good_files will successfully upload | ||||||
|  |     // nil_files are empty files and will show an alert in the final hook | ||||||
|  |     // bad_files are unreadable and cannot be uploaded | ||||||
|  |     var file_lists = [good_files, nil_files, bad_files]; | ||||||
|  |  | ||||||
|  |     // build a list of filenames | ||||||
|  |     var filenames = []; | ||||||
|  |     for (var lst of file_lists) | ||||||
|  |         for (var ent of lst) | ||||||
|  |             filenames.push(ent[1]); | ||||||
|  |  | ||||||
|  |     toast.inf(5, "running database query..."); | ||||||
|  |  | ||||||
|  |     // simulate delay while passing the list to some api for checking | ||||||
|  |     setTimeout(function () { | ||||||
|  |  | ||||||
|  |         // only keep webm files as an example | ||||||
|  |         var new_lists = []; | ||||||
|  |         for (var lst of file_lists) { | ||||||
|  |             var keep = []; | ||||||
|  |             new_lists.push(keep); | ||||||
|  |  | ||||||
|  |             for (var ent of lst) | ||||||
|  |                 if (/\.webm$/.test(ent[1])) | ||||||
|  |                     keep.push(ent); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // finally, call the next hook in the chain | ||||||
|  |         [good_files, nil_files, bad_files] = new_lists; | ||||||
|  |         hooks[0](good_files, nil_files, bad_files, hooks.slice(1)); | ||||||
|  |  | ||||||
|  |     }, 1000); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // register | ||||||
|  | up2k_hooks.push(function () { | ||||||
|  |     up2k.gotallfiles.unshift(up2k_namefilter); | ||||||
|  | }); | ||||||
							
								
								
									
										31
									
								
								contrib/rc/copyparty
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								contrib/rc/copyparty
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | #!/bin/sh | ||||||
|  | # | ||||||
|  | # PROVIDE: copyparty | ||||||
|  | # REQUIRE: networking | ||||||
|  | # KEYWORD: | ||||||
|  |  | ||||||
|  | . /etc/rc.subr | ||||||
|  |  | ||||||
|  | name="copyparty" | ||||||
|  | rcvar="copyparty_enable" | ||||||
|  | copyparty_user="copyparty" | ||||||
|  | copyparty_args="-e2dsa -v /storage:/storage:r" # change as you see fit | ||||||
|  | copyparty_command="/usr/local/bin/python3.8 /usr/local/copyparty/copyparty-sfx.py ${copyparty_args}" | ||||||
|  | pidfile="/var/run/copyparty/${name}.pid" | ||||||
|  | command="/usr/sbin/daemon" | ||||||
|  | command_args="-P ${pidfile} -r -f ${copyparty_command}" | ||||||
|  |  | ||||||
|  | stop_postcmd="copyparty_shutdown" | ||||||
|  |  | ||||||
|  | copyparty_shutdown() | ||||||
|  | { | ||||||
|  |         if [ -e "${pidfile}" ]; then | ||||||
|  |                 echo "Stopping supervising daemon." | ||||||
|  |                 kill -s TERM `cat ${pidfile}` | ||||||
|  |         fi | ||||||
|  | } | ||||||
|  |  | ||||||
|  | load_rc_config $name | ||||||
|  | : ${copyparty_enable:=no} | ||||||
|  |  | ||||||
|  | run_rc_command "$1" | ||||||
							
								
								
									
										26
									
								
								contrib/systemd/cfssl.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								contrib/systemd/cfssl.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | # NOTE: this is now a built-in feature in copyparty | ||||||
|  | # but you may still want this if you have specific needs | ||||||
|  | # | ||||||
|  | # systemd service which generates a new TLS certificate on each boot, | ||||||
|  | # that way the one-year expiry time won't cause any issues -- | ||||||
|  | # just have everyone trust the ca.pem once every 10 years | ||||||
|  | # | ||||||
|  | # assumptions/placeholder values: | ||||||
|  | #  * this script and copyparty runs as user "cpp" | ||||||
|  | #  * copyparty repo is at ~cpp/dev/copyparty | ||||||
|  | #  * CA is named partylan | ||||||
|  | #  * server IPs = 10.1.2.3 and 192.168.123.1 | ||||||
|  | #  * server hostname = party.lan | ||||||
|  |  | ||||||
|  | [Unit] | ||||||
|  | Description=copyparty certificate generator | ||||||
|  | Before=copyparty.service | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | User=cpp | ||||||
|  | Type=oneshot | ||||||
|  | SyslogIdentifier=cpp-cert | ||||||
|  | ExecStart=/bin/bash -c 'cd ~/dev/copyparty/contrib && ./cfssl.sh partylan 10.1.2.3,192.168.123.1,party.lan y' | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
| @@ -2,11 +2,27 @@ | |||||||
| # and share '/mnt' with anonymous read+write | # and share '/mnt' with anonymous read+write | ||||||
| # | # | ||||||
| # installation: | # installation: | ||||||
| #   cp -pv copyparty.service /etc/systemd/system && systemctl enable --now copyparty | #   wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py | ||||||
|  | #   cp -pv copyparty.service /etc/systemd/system/ | ||||||
|  | #   restorecon -vr /etc/systemd/system/copyparty.service  # on fedora/rhel | ||||||
|  | #   firewall-cmd --permanent --add-port={80,443,3923}/tcp  # --zone=libvirt | ||||||
|  | #   firewall-cmd --reload | ||||||
|  | #   systemctl daemon-reload && systemctl enable --now copyparty | ||||||
|  | # | ||||||
|  | # if it fails to start, first check this: systemctl status copyparty | ||||||
|  | # then try starting it while viewing logs: journalctl -fan 100 | ||||||
| # | # | ||||||
| # you may want to: | # you may want to: | ||||||
| #   change '/usr/bin/python' to another interpreter | #   change "User=cpp" and "/home/cpp/" to another user | ||||||
| #   change '/mnt::a' to another location or permission-set | #   remove the nft lines to only listen on port 3923 | ||||||
|  | # and in the ExecStart= line: | ||||||
|  | #   change '/usr/bin/python3' to another interpreter | ||||||
|  | #   change '/mnt::rw' to another location or permission-set | ||||||
|  | #   add '-q' to disable logging on busy servers | ||||||
|  | #   add '-i 127.0.0.1' to only allow local connections | ||||||
|  | #   add '-e2dsa' to enable filesystem scanning + indexing | ||||||
|  | #   add '-e2ts' to enable metadata indexing | ||||||
|  | #   remove '--ansi' to disable colored logs | ||||||
| # | # | ||||||
| # with `Type=notify`, copyparty will signal systemd when it is ready to | # with `Type=notify`, copyparty will signal systemd when it is ready to | ||||||
| #   accept connections; correctly delaying units depending on copyparty. | #   accept connections; correctly delaying units depending on copyparty. | ||||||
| @@ -14,12 +30,11 @@ | |||||||
| #   python disabling line-buffering, so messages are out-of-order: | #   python disabling line-buffering, so messages are out-of-order: | ||||||
| #   https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png | #   https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png | ||||||
| # | # | ||||||
| # enable line-buffering for realtime logging (slight performance cost): | # unless you add -q to disable logging, you may want to remove the | ||||||
| #   modify ExecStart and prefix it with `/usr/bin/stdbuf -oL` like so: | #   following line to allow buffering (slightly better performance): | ||||||
| #   ExecStart=/usr/bin/stdbuf -oL /usr/bin/python3 [...] |  | ||||||
| # but some systemd versions require this instead (higher performance cost): |  | ||||||
| #   inside the [Service] block, add the following line: |  | ||||||
| #   Environment=PYTHONUNBUFFERED=x | #   Environment=PYTHONUNBUFFERED=x | ||||||
|  | # | ||||||
|  | # keep ExecStartPre before ExecStart, at least on rhel8 | ||||||
|  |  | ||||||
| [Unit] | [Unit] | ||||||
| Description=copyparty file server | Description=copyparty file server | ||||||
| @@ -27,8 +42,25 @@ Description=copyparty file server | |||||||
| [Service] | [Service] | ||||||
| Type=notify | Type=notify | ||||||
| SyslogIdentifier=copyparty | SyslogIdentifier=copyparty | ||||||
| ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a | Environment=PYTHONUNBUFFERED=x | ||||||
| ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' | ExecReload=/bin/kill -s USR1 $MAINPID | ||||||
|  |  | ||||||
|  | # user to run as + where the TLS certificate is (if any) | ||||||
|  | User=cpp | ||||||
|  | Environment=XDG_CONFIG_HOME=/home/cpp/.config | ||||||
|  |  | ||||||
|  | # OPTIONAL: setup forwarding from ports 80 and 443 to port 3923 | ||||||
|  | ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true' | ||||||
|  | ExecStartPre=+nft add table ip nat | ||||||
|  | ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; } | ||||||
|  | ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923 | ||||||
|  | ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923 | ||||||
|  |  | ||||||
|  | # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running | ||||||
|  | ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' | ||||||
|  |  | ||||||
|  | # copyparty settings | ||||||
|  | ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py --ansi -e2d -v /mnt::rw | ||||||
|  |  | ||||||
| [Install] | [Install] | ||||||
| WantedBy=multi-user.target | WantedBy=multi-user.target | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								contrib/systemd/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								contrib/systemd/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | # this will start `/usr/local/bin/copyparty-sfx.py` | ||||||
|  | # in a chroot, preventing accidental access elsewhere | ||||||
|  | # and share '/mnt' with anonymous read+write | ||||||
|  | # | ||||||
|  | # installation: | ||||||
|  | #   1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin | ||||||
|  | #   2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty | ||||||
|  | # | ||||||
|  | # expose additional filesystem locations to copyparty | ||||||
|  | #   by listing them between the last `1000` and `--` | ||||||
|  | # | ||||||
|  | # `1000 1000` = what user to run copyparty as | ||||||
|  | # | ||||||
|  | # you may want to: | ||||||
|  | #   change '/mnt::rw' to another location or permission-set | ||||||
|  | #    (remember to change the '/mnt' chroot arg too) | ||||||
|  | # | ||||||
|  | # unless you add -q to disable logging, you may want to remove the | ||||||
|  | #   following line to allow buffering (slightly better performance): | ||||||
|  | #   Environment=PYTHONUNBUFFERED=x | ||||||
|  |  | ||||||
|  | [Unit] | ||||||
|  | Description=copyparty file server | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | SyslogIdentifier=prisonparty | ||||||
|  | Environment=PYTHONUNBUFFERED=x | ||||||
|  | WorkingDirectory=/var/lib/copyparty-jail | ||||||
|  | ExecReload=/bin/kill -s USR1 $MAINPID | ||||||
|  |  | ||||||
|  | # stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running | ||||||
|  | ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' | ||||||
|  |  | ||||||
|  | # run copyparty | ||||||
|  | ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \ | ||||||
|  |   /usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
							
								
								
									
										45
									
								
								contrib/webdav-cfg.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								contrib/webdav-cfg.bat
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | @echo off | ||||||
|  | rem removes the 47.6 MiB filesize limit when downloading from webdav | ||||||
|  | rem + optionally allows/enables password-auth over plaintext http | ||||||
|  | rem + optionally helps disable wpad, removing the 10sec latency | ||||||
|  |  | ||||||
|  | net session >nul 2>&1 | ||||||
|  | if %errorlevel% neq 0 ( | ||||||
|  |     echo sorry, you must run this as administrator | ||||||
|  |     pause | ||||||
|  |     exit /b | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v FileSizeLimitInBytes /t REG_DWORD /d 0xffffffff /f | ||||||
|  | reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters /v FsCtlRequestTimeoutInSec /t REG_DWORD /d 0xffffffff /f | ||||||
|  |  | ||||||
|  | echo( | ||||||
|  | echo OK; | ||||||
|  | echo allow webdav basic-auth over plaintext http? | ||||||
|  | echo Y: login works, but the password will be visible in wireshark etc | ||||||
|  | echo N: login will NOT work unless you use https and valid certificates | ||||||
|  | choice | ||||||
|  | if %errorlevel% equ 1 ( | ||||||
|  |     reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f | ||||||
|  |     rem default is 1 (require tls) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | echo( | ||||||
|  | echo OK; | ||||||
|  | echo do you want to disable wpad? | ||||||
|  | echo can give a HUGE speed boost depending on network settings | ||||||
|  | choice | ||||||
|  | if %errorlevel% equ 1 ( | ||||||
|  |     echo( | ||||||
|  |     echo i'm about to open the [Connections] tab in [Internet Properties] for you; | ||||||
|  |     echo please click [LAN settings] and disable [Automatically detect settings] | ||||||
|  |     echo( | ||||||
|  |     pause | ||||||
|  |     control inetcpl.cpl,,4 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | net stop webclient | ||||||
|  | net start webclient | ||||||
|  | echo( | ||||||
|  | echo OK; all done | ||||||
|  | pause | ||||||
| @@ -1,53 +1,62 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
| import platform |  | ||||||
| import time |  | ||||||
| import sys |  | ||||||
| import os | import os | ||||||
|  | import platform | ||||||
|  | import sys | ||||||
|  | import time | ||||||
|  |  | ||||||
| PY2 = sys.version_info[0] == 2 | # fmt: off | ||||||
| if PY2: | _:tuple[int,int]=(0,0)  # _____________________________________________________________________  hey there! if you are reading this, your python is too old to run copyparty without some help. Please use https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py or the pypi package instead, or see https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building if you want to build it yourself :-)  ************************************************************************************************************************************************ | ||||||
|     sys.dont_write_bytecode = True | # fmt: on | ||||||
|     unicode = unicode |  | ||||||
|  | try: | ||||||
|  |     from typing import TYPE_CHECKING | ||||||
|  | except: | ||||||
|  |     TYPE_CHECKING = False | ||||||
|  |  | ||||||
|  | if True: | ||||||
|  |     from typing import Any, Callable | ||||||
|  |  | ||||||
|  | PY2 = sys.version_info < (3,) | ||||||
|  | if not PY2: | ||||||
|  |     unicode: Callable[[Any], str] = str | ||||||
| else: | else: | ||||||
|     unicode = str |     sys.dont_write_bytecode = True | ||||||
|  |     unicode = unicode  # noqa: F821  # pylint: disable=undefined-variable,self-assigning-variable | ||||||
|  |  | ||||||
| WINDOWS = False | WINDOWS: Any = ( | ||||||
| if platform.system() == "Windows": |     [int(x) for x in platform.version().split(".")] | ||||||
|     WINDOWS = [int(x) for x in platform.version().split(".")] |     if platform.system() == "Windows" | ||||||
|  |     else False | ||||||
|  | ) | ||||||
|  |  | ||||||
| VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393] | VT100 = "--ansi" in sys.argv or ( | ||||||
|  |     os.environ.get("NO_COLOR", "").lower() in ("", "0", "false") | ||||||
|  |     and sys.stdout.isatty() | ||||||
|  |     and "--no-ansi" not in sys.argv | ||||||
|  |     and (not WINDOWS or WINDOWS >= [10, 0, 14393]) | ||||||
|  | ) | ||||||
| # introduced in anniversary update | # introduced in anniversary update | ||||||
|  |  | ||||||
| ANYWIN = WINDOWS or sys.platform in ["msys"] | ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"] | ||||||
|  |  | ||||||
| MACOS = platform.system() == "Darwin" | MACOS = platform.system() == "Darwin" | ||||||
|  |  | ||||||
|  | EXE = bool(getattr(sys, "frozen", False)) | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     CORES = len(os.sched_getaffinity(0)) | ||||||
|  | except: | ||||||
|  |     CORES = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2 | ||||||
|  |  | ||||||
|  |  | ||||||
| class EnvParams(object): | class EnvParams(object): | ||||||
|     def __init__(self): |     def __init__(self) -> None: | ||||||
|         self.t0 = time.time() |         self.t0 = time.time() | ||||||
|         self.mod = os.path.dirname(os.path.realpath(__file__)) |         self.mod = "" | ||||||
|         if self.mod.endswith("__init__"): |         self.cfg = "" | ||||||
|             self.mod = os.path.dirname(self.mod) |         self.ox = getattr(sys, "oxidized", None) | ||||||
|  |  | ||||||
|         if sys.platform == "win32": |  | ||||||
|             self.cfg = os.path.normpath(os.environ["APPDATA"] + "/copyparty") |  | ||||||
|         elif sys.platform == "darwin": |  | ||||||
|             self.cfg = os.path.expanduser("~/Library/Preferences/copyparty") |  | ||||||
|         else: |  | ||||||
|             self.cfg = os.path.normpath( |  | ||||||
|                 os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) |  | ||||||
|                 + "/copyparty" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         self.cfg = self.cfg.replace("\\", "/") |  | ||||||
|         try: |  | ||||||
|             os.makedirs(self.cfg) |  | ||||||
|         except: |  | ||||||
|             if not os.path.isdir(self.cfg): |  | ||||||
|                 raise |  | ||||||
|  |  | ||||||
|  |  | ||||||
| E = EnvParams() | E = EnvParams() | ||||||
|   | |||||||
							
								
								
									
										1302
									
								
								copyparty/__main__.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										1302
									
								
								copyparty/__main__.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
|  |  | ||||||
| VERSION = (0, 13, 0) | VERSION = (1, 9, 6) | ||||||
| CODENAME = "future-proof" | CODENAME = "prometheable" | ||||||
| BUILD_DT = (2021, 8, 8) | BUILD_DT = (2023, 9, 23) | ||||||
|  |  | ||||||
| S_VERSION = ".".join(map(str, VERSION)) | S_VERSION = ".".join(map(str, VERSION)) | ||||||
| S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) | S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) | ||||||
|   | |||||||
							
								
								
									
										1810
									
								
								copyparty/authsrv.py
									
									
									
									
									
								
							
							
						
						
									
										1810
									
								
								copyparty/authsrv.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,58 +2,80 @@ | |||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
| import os | import os | ||||||
| from ..util import fsenc, fsdec |  | ||||||
| from . import path |  | ||||||
|  |  | ||||||
|  | from ..util import SYMTIME, fsdec, fsenc | ||||||
|  | from . import path as path | ||||||
|  |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from typing import Any, Optional | ||||||
|  |  | ||||||
|  | _ = (path,) | ||||||
|  | __all__ = ["path"] | ||||||
|  |  | ||||||
| # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c | # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c | ||||||
| # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" | # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')" | ||||||
|  |  | ||||||
|  |  | ||||||
| def chmod(p, mode): | def chmod(p: str, mode: int) -> None: | ||||||
|     return os.chmod(fsenc(p), mode) |     return os.chmod(fsenc(p), mode) | ||||||
|  |  | ||||||
|  |  | ||||||
| def listdir(p="."): | def listdir(p: str = ".") -> list[str]: | ||||||
|     return [fsdec(x) for x in os.listdir(fsenc(p))] |     return [fsdec(x) for x in os.listdir(fsenc(p))] | ||||||
|  |  | ||||||
|  |  | ||||||
| def lstat(p): | def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool: | ||||||
|     return os.lstat(fsenc(p)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def makedirs(name, mode=0o755, exist_ok=True): |  | ||||||
|     bname = fsenc(name) |     bname = fsenc(name) | ||||||
|     try: |     try: | ||||||
|         os.makedirs(bname, mode=mode) |         os.makedirs(bname, mode) | ||||||
|  |         return True | ||||||
|     except: |     except: | ||||||
|         if not exist_ok or not os.path.isdir(bname): |         if not exist_ok or not os.path.isdir(bname): | ||||||
|             raise |             raise | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
| def mkdir(p, mode=0o755): | def mkdir(p: str, mode: int = 0o755) -> None: | ||||||
|     return os.mkdir(fsenc(p), mode=mode) |     return os.mkdir(fsenc(p), mode) | ||||||
|  |  | ||||||
|  |  | ||||||
| def rename(src, dst): | def open(p: str, *a, **ka) -> int: | ||||||
|  |     return os.open(fsenc(p), *a, **ka) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def rename(src: str, dst: str) -> None: | ||||||
|     return os.rename(fsenc(src), fsenc(dst)) |     return os.rename(fsenc(src), fsenc(dst)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def replace(src, dst): | def replace(src: str, dst: str) -> None: | ||||||
|     return os.replace(fsenc(src), fsenc(dst)) |     return os.replace(fsenc(src), fsenc(dst)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def rmdir(p): | def rmdir(p: str) -> None: | ||||||
|     return os.rmdir(fsenc(p)) |     return os.rmdir(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def stat(p): | def stat(p: str) -> os.stat_result: | ||||||
|     return os.stat(fsenc(p)) |     return os.stat(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def unlink(p): | def unlink(p: str) -> None: | ||||||
|     return os.unlink(fsenc(p)) |     return os.unlink(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def utime(p, times=None): | def utime( | ||||||
|     return os.utime(fsenc(p), times) |     p: str, times: Optional[tuple[float, float]] = None, follow_symlinks: bool = True | ||||||
|  | ) -> None: | ||||||
|  |     if SYMTIME: | ||||||
|  |         return os.utime(fsenc(p), times, follow_symlinks=follow_symlinks) | ||||||
|  |     else: | ||||||
|  |         return os.utime(fsenc(p), times) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if hasattr(os, "lstat"): | ||||||
|  |  | ||||||
|  |     def lstat(p: str) -> os.stat_result: | ||||||
|  |         return os.lstat(fsenc(p)) | ||||||
|  |  | ||||||
|  | else: | ||||||
|  |     lstat = stat | ||||||
|   | |||||||
| @@ -2,32 +2,44 @@ | |||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
| import os | import os | ||||||
| from ..util import fsenc, fsdec |  | ||||||
|  | from ..util import SYMTIME, fsdec, fsenc | ||||||
|  |  | ||||||
|  |  | ||||||
| def abspath(p): | def abspath(p: str) -> str: | ||||||
|     return fsdec(os.path.abspath(fsenc(p))) |     return fsdec(os.path.abspath(fsenc(p))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def exists(p): | def exists(p: str) -> bool: | ||||||
|     return os.path.exists(fsenc(p)) |     return os.path.exists(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def getmtime(p): | def getmtime(p: str, follow_symlinks: bool = True) -> float: | ||||||
|     return os.path.getmtime(fsenc(p)) |     if not follow_symlinks and SYMTIME: | ||||||
|  |         return os.lstat(fsenc(p)).st_mtime | ||||||
|  |     else: | ||||||
|  |         return os.path.getmtime(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def getsize(p): | def getsize(p: str) -> int: | ||||||
|     return os.path.getsize(fsenc(p)) |     return os.path.getsize(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def isdir(p): | def isfile(p: str) -> bool: | ||||||
|  |     return os.path.isfile(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def isdir(p: str) -> bool: | ||||||
|     return os.path.isdir(fsenc(p)) |     return os.path.isdir(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def islink(p): | def islink(p: str) -> bool: | ||||||
|     return os.path.islink(fsenc(p)) |     return os.path.islink(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def realpath(p): | def lexists(p: str) -> bool: | ||||||
|  |     return os.path.lexists(fsenc(p)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def realpath(p: str) -> str: | ||||||
|     return fsdec(os.path.realpath(fsenc(p))) |     return fsdec(os.path.realpath(fsenc(p))) | ||||||
|   | |||||||
| @@ -1,52 +1,64 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
| import time |  | ||||||
| import threading | import threading | ||||||
|  | import time | ||||||
|  | import traceback | ||||||
|  |  | ||||||
| from .broker_util import try_exec | import queue | ||||||
|  |  | ||||||
|  | from .__init__ import CORES, TYPE_CHECKING | ||||||
| from .broker_mpw import MpWorker | from .broker_mpw import MpWorker | ||||||
| from .util import mp | from .broker_util import ExceptionalQueue, try_exec | ||||||
|  | from .util import Daemon, mp | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from .svchub import SvcHub | ||||||
|  |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from typing import Any | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MProcess(mp.Process): | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         q_pend: queue.Queue[tuple[int, str, list[Any]]], | ||||||
|  |         q_yield: queue.Queue[tuple[int, str, list[Any]]], | ||||||
|  |         target: Any, | ||||||
|  |         args: Any, | ||||||
|  |     ) -> None: | ||||||
|  |         super(MProcess, self).__init__(target=target, args=args) | ||||||
|  |         self.q_pend = q_pend | ||||||
|  |         self.q_yield = q_yield | ||||||
|  |  | ||||||
|  |  | ||||||
| class BrokerMp(object): | class BrokerMp(object): | ||||||
|     """external api; manages MpWorkers""" |     """external api; manages MpWorkers""" | ||||||
|  |  | ||||||
|     def __init__(self, hub): |     def __init__(self, hub: "SvcHub") -> None: | ||||||
|         self.hub = hub |         self.hub = hub | ||||||
|         self.log = hub.log |         self.log = hub.log | ||||||
|         self.args = hub.args |         self.args = hub.args | ||||||
|  |  | ||||||
|         self.procs = [] |         self.procs = [] | ||||||
|         self.retpend = {} |  | ||||||
|         self.retpend_mutex = threading.Lock() |  | ||||||
|         self.mutex = threading.Lock() |         self.mutex = threading.Lock() | ||||||
|  |  | ||||||
|         self.num_workers = self.args.j or mp.cpu_count() |         self.num_workers = self.args.j or CORES | ||||||
|         self.log("broker", "booting {} subprocesses".format(self.num_workers)) |         self.log("broker", "booting {} subprocesses".format(self.num_workers)) | ||||||
|         for n in range(1, self.num_workers + 1): |         for n in range(1, self.num_workers + 1): | ||||||
|             q_pend = mp.Queue(1) |             q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1) | ||||||
|             q_yield = mp.Queue(64) |             q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64) | ||||||
|  |  | ||||||
|             proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n)) |  | ||||||
|             proc.q_pend = q_pend |  | ||||||
|             proc.q_yield = q_yield |  | ||||||
|             proc.clients = {} |  | ||||||
|  |  | ||||||
|             thr = threading.Thread( |  | ||||||
|                 target=self.collector, args=(proc,), name="mp-sink-{}".format(n) |  | ||||||
|             ) |  | ||||||
|             thr.daemon = True |  | ||||||
|             thr.start() |  | ||||||
|  |  | ||||||
|  |             proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n)) | ||||||
|  |             Daemon(self.collector, "mp-sink-{}".format(n), (proc,)) | ||||||
|             self.procs.append(proc) |             self.procs.append(proc) | ||||||
|             proc.start() |             proc.start() | ||||||
|  |  | ||||||
|     def shutdown(self): |     def shutdown(self) -> None: | ||||||
|         self.log("broker", "shutting down") |         self.log("broker", "shutting down") | ||||||
|         for n, proc in enumerate(self.procs): |         for n, proc in enumerate(self.procs): | ||||||
|             thr = threading.Thread( |             thr = threading.Thread( | ||||||
|                 target=proc.q_pend.put([0, "shutdown", []]), |                 target=proc.q_pend.put((0, "shutdown", [])), | ||||||
|                 name="mp-shutdown-{}-{}".format(n, len(self.procs)), |                 name="mp-shutdown-{}-{}".format(n, len(self.procs)), | ||||||
|             ) |             ) | ||||||
|             thr.start() |             thr.start() | ||||||
| @@ -57,12 +69,17 @@ class BrokerMp(object): | |||||||
|  |  | ||||||
|         while procs: |         while procs: | ||||||
|             if procs[-1].is_alive(): |             if procs[-1].is_alive(): | ||||||
|                 time.sleep(0.1) |                 time.sleep(0.05) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             procs.pop() |             procs.pop() | ||||||
|  |  | ||||||
|     def collector(self, proc): |     def reload(self) -> None: | ||||||
|  |         self.log("broker", "reloading") | ||||||
|  |         for _, proc in enumerate(self.procs): | ||||||
|  |             proc.q_pend.put((0, "reload", [])) | ||||||
|  |  | ||||||
|  |     def collector(self, proc: MProcess) -> None: | ||||||
|         """receive message from hub in other process""" |         """receive message from hub in other process""" | ||||||
|         while True: |         while True: | ||||||
|             msg = proc.q_yield.get() |             msg = proc.q_yield.get() | ||||||
| @@ -73,24 +90,37 @@ class BrokerMp(object): | |||||||
|  |  | ||||||
|             elif dest == "retq": |             elif dest == "retq": | ||||||
|                 # response from previous ipc call |                 # response from previous ipc call | ||||||
|                 with self.retpend_mutex: |                 raise Exception("invalid broker_mp usage") | ||||||
|                     retq = self.retpend.pop(retq_id) |  | ||||||
|  |  | ||||||
|                 retq.put(args) |  | ||||||
|  |  | ||||||
|             else: |             else: | ||||||
|                 # new ipc invoking managed service in hub |                 # new ipc invoking managed service in hub | ||||||
|                 obj = self.hub |                 try: | ||||||
|                 for node in dest.split("."): |                     obj = self.hub | ||||||
|                     obj = getattr(obj, node) |                     for node in dest.split("."): | ||||||
|  |                         obj = getattr(obj, node) | ||||||
|  |  | ||||||
|                 # TODO will deadlock if dest performs another ipc |                     # TODO will deadlock if dest performs another ipc | ||||||
|                 rv = try_exec(retq_id, obj, *args) |                     rv = try_exec(retq_id, obj, *args) | ||||||
|  |                 except: | ||||||
|  |                     rv = ["exception", "stack", traceback.format_exc()] | ||||||
|  |  | ||||||
|                 if retq_id: |                 if retq_id: | ||||||
|                     proc.q_pend.put([retq_id, "retq", rv]) |                     proc.q_pend.put((retq_id, "retq", rv)) | ||||||
|  |  | ||||||
|     def put(self, want_retval, dest, *args): |     def ask(self, dest: str, *args: Any) -> ExceptionalQueue: | ||||||
|  |  | ||||||
|  |         # new non-ipc invoking managed service in hub | ||||||
|  |         obj = self.hub | ||||||
|  |         for node in dest.split("."): | ||||||
|  |             obj = getattr(obj, node) | ||||||
|  |  | ||||||
|  |         rv = try_exec(True, obj, *args) | ||||||
|  |  | ||||||
|  |         retq = ExceptionalQueue(1) | ||||||
|  |         retq.put(rv) | ||||||
|  |         return retq | ||||||
|  |  | ||||||
|  |     def say(self, dest: str, *args: Any) -> None: | ||||||
|         """ |         """ | ||||||
|         send message to non-hub component in other process, |         send message to non-hub component in other process, | ||||||
|         returns a Queue object which eventually contains the response if want_retval |         returns a Queue object which eventually contains the response if want_retval | ||||||
| @@ -98,7 +128,11 @@ class BrokerMp(object): | |||||||
|         """ |         """ | ||||||
|         if dest == "listen": |         if dest == "listen": | ||||||
|             for p in self.procs: |             for p in self.procs: | ||||||
|                 p.q_pend.put([0, dest, [args[0], len(self.procs)]]) |                 p.q_pend.put((0, dest, [args[0], len(self.procs)])) | ||||||
|  |  | ||||||
|  |         elif dest == "set_netdevs": | ||||||
|  |             for p in self.procs: | ||||||
|  |                 p.q_pend.put((0, dest, list(args))) | ||||||
|  |  | ||||||
|         elif dest == "cb_httpsrv_up": |         elif dest == "cb_httpsrv_up": | ||||||
|             self.hub.cb_httpsrv_up() |             self.hub.cb_httpsrv_up() | ||||||
|   | |||||||
| @@ -1,20 +1,38 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
| import sys | import argparse | ||||||
|  | import os | ||||||
| import signal | import signal | ||||||
|  | import sys | ||||||
| import threading | import threading | ||||||
|  |  | ||||||
| from .broker_util import ExceptionalQueue | import queue | ||||||
|  |  | ||||||
|  | from .__init__ import ANYWIN | ||||||
|  | from .authsrv import AuthSrv | ||||||
|  | from .broker_util import BrokerCli, ExceptionalQueue | ||||||
| from .httpsrv import HttpSrv | from .httpsrv import HttpSrv | ||||||
| from .util import FAKE_MP | from .util import FAKE_MP, Daemon, HMaccas | ||||||
| from copyparty.authsrv import AuthSrv |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from types import FrameType | ||||||
|  |  | ||||||
|  |     from typing import Any, Optional, Union | ||||||
|  |  | ||||||
|  |  | ||||||
| class MpWorker(object): | class MpWorker(BrokerCli): | ||||||
|     """one single mp instance""" |     """one single mp instance""" | ||||||
|  |  | ||||||
|     def __init__(self, q_pend, q_yield, args, n): |     def __init__( | ||||||
|  |         self, | ||||||
|  |         q_pend: queue.Queue[tuple[int, str, list[Any]]], | ||||||
|  |         q_yield: queue.Queue[tuple[int, str, list[Any]]], | ||||||
|  |         args: argparse.Namespace, | ||||||
|  |         n: int, | ||||||
|  |     ) -> None: | ||||||
|  |         super(MpWorker, self).__init__() | ||||||
|  |  | ||||||
|         self.q_pend = q_pend |         self.q_pend = q_pend | ||||||
|         self.q_yield = q_yield |         self.q_yield = q_yield | ||||||
|         self.args = args |         self.args = args | ||||||
| @@ -22,43 +40,45 @@ class MpWorker(object): | |||||||
|  |  | ||||||
|         self.log = self._log_disabled if args.q and not args.lo else self._log_enabled |         self.log = self._log_disabled if args.q and not args.lo else self._log_enabled | ||||||
|  |  | ||||||
|         self.retpend = {} |         self.retpend: dict[int, Any] = {} | ||||||
|         self.retpend_mutex = threading.Lock() |         self.retpend_mutex = threading.Lock() | ||||||
|         self.mutex = threading.Lock() |         self.mutex = threading.Lock() | ||||||
|  |  | ||||||
|         # we inherited signal_handler from parent, |         # we inherited signal_handler from parent, | ||||||
|         # replace it with something harmless |         # replace it with something harmless | ||||||
|         if not FAKE_MP: |         if not FAKE_MP: | ||||||
|             for sig in [signal.SIGINT, signal.SIGTERM]: |             sigs = [signal.SIGINT, signal.SIGTERM] | ||||||
|  |             if not ANYWIN: | ||||||
|  |                 sigs.append(signal.SIGUSR1) | ||||||
|  |  | ||||||
|  |             for sig in sigs: | ||||||
|                 signal.signal(sig, self.signal_handler) |                 signal.signal(sig, self.signal_handler) | ||||||
|  |  | ||||||
|         # starting to look like a good idea |         # starting to look like a good idea | ||||||
|         self.asrv = AuthSrv(args, None, False) |         self.asrv = AuthSrv(args, None, False) | ||||||
|  |  | ||||||
|         # instantiate all services here (TODO: inheritance?) |         # instantiate all services here (TODO: inheritance?) | ||||||
|  |         self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8) | ||||||
|         self.httpsrv = HttpSrv(self, n) |         self.httpsrv = HttpSrv(self, n) | ||||||
|  |  | ||||||
|         # on winxp and some other platforms, |         # on winxp and some other platforms, | ||||||
|         # use thr.join() to block all signals |         # use thr.join() to block all signals | ||||||
|         thr = threading.Thread(target=self.main, name="mpw-main") |         Daemon(self.main, "mpw-main").join() | ||||||
|         thr.daemon = True |  | ||||||
|         thr.start() |  | ||||||
|         thr.join() |  | ||||||
|  |  | ||||||
|     def signal_handler(self, sig, frame): |     def signal_handler(self, sig: Optional[int], frame: Optional[FrameType]) -> None: | ||||||
|         # print('k') |         # print('k') | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     def _log_enabled(self, src, msg, c=0): |     def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: | ||||||
|         self.q_yield.put([0, "log", [src, msg, c]]) |         self.q_yield.put((0, "log", [src, msg, c])) | ||||||
|  |  | ||||||
|     def _log_disabled(self, src, msg, c=0): |     def _log_disabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     def logw(self, msg, c=0): |     def logw(self, msg: str, c: Union[int, str] = 0) -> None: | ||||||
|         self.log("mp{}".format(self.n), msg, c) |         self.log("mp{}".format(self.n), msg, c) | ||||||
|  |  | ||||||
|     def main(self): |     def main(self) -> None: | ||||||
|         while True: |         while True: | ||||||
|             retq_id, dest, args = self.q_pend.get() |             retq_id, dest, args = self.q_pend.get() | ||||||
|  |  | ||||||
| @@ -69,9 +89,17 @@ class MpWorker(object): | |||||||
|                 sys.exit(0) |                 sys.exit(0) | ||||||
|                 return |                 return | ||||||
|  |  | ||||||
|  |             elif dest == "reload": | ||||||
|  |                 self.logw("mpw.asrv reloading") | ||||||
|  |                 self.asrv.reload() | ||||||
|  |                 self.logw("mpw.asrv reloaded") | ||||||
|  |  | ||||||
|             elif dest == "listen": |             elif dest == "listen": | ||||||
|                 self.httpsrv.listen(args[0], args[1]) |                 self.httpsrv.listen(args[0], args[1]) | ||||||
|  |  | ||||||
|  |             elif dest == "set_netdevs": | ||||||
|  |                 self.httpsrv.set_netdevs(args[0]) | ||||||
|  |  | ||||||
|             elif dest == "retq": |             elif dest == "retq": | ||||||
|                 # response from previous ipc call |                 # response from previous ipc call | ||||||
|                 with self.retpend_mutex: |                 with self.retpend_mutex: | ||||||
| @@ -82,15 +110,14 @@ class MpWorker(object): | |||||||
|             else: |             else: | ||||||
|                 raise Exception("what is " + str(dest)) |                 raise Exception("what is " + str(dest)) | ||||||
|  |  | ||||||
|     def put(self, want_retval, dest, *args): |     def ask(self, dest: str, *args: Any) -> ExceptionalQueue: | ||||||
|         if want_retval: |         retq = ExceptionalQueue(1) | ||||||
|             retq = ExceptionalQueue(1) |         retq_id = id(retq) | ||||||
|             retq_id = id(retq) |         with self.retpend_mutex: | ||||||
|             with self.retpend_mutex: |             self.retpend[retq_id] = retq | ||||||
|                 self.retpend[retq_id] = retq |  | ||||||
|         else: |  | ||||||
|             retq = None |  | ||||||
|             retq_id = 0 |  | ||||||
|  |  | ||||||
|         self.q_yield.put([retq_id, dest, args]) |         self.q_yield.put((retq_id, dest, list(args))) | ||||||
|         return retq |         return retq | ||||||
|  |  | ||||||
|  |     def say(self, dest: str, *args: Any) -> None: | ||||||
|  |         self.q_yield.put((0, dest, list(args))) | ||||||
|   | |||||||
| @@ -1,16 +1,27 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
|  | import os | ||||||
| import threading | import threading | ||||||
|  |  | ||||||
|  | from .__init__ import TYPE_CHECKING | ||||||
|  | from .broker_util import BrokerCli, ExceptionalQueue, try_exec | ||||||
| from .httpsrv import HttpSrv | from .httpsrv import HttpSrv | ||||||
| from .broker_util import ExceptionalQueue, try_exec | from .util import HMaccas | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from .svchub import SvcHub | ||||||
|  |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from typing import Any | ||||||
|  |  | ||||||
|  |  | ||||||
| class BrokerThr(object): | class BrokerThr(BrokerCli): | ||||||
|     """external api; behaves like BrokerMP but using plain threads""" |     """external api; behaves like BrokerMP but using plain threads""" | ||||||
|  |  | ||||||
|     def __init__(self, hub): |     def __init__(self, hub: "SvcHub") -> None: | ||||||
|  |         super(BrokerThr, self).__init__() | ||||||
|  |  | ||||||
|         self.hub = hub |         self.hub = hub | ||||||
|         self.log = hub.log |         self.log = hub.log | ||||||
|         self.args = hub.args |         self.args = hub.args | ||||||
| @@ -20,29 +31,43 @@ class BrokerThr(object): | |||||||
|         self.num_workers = 1 |         self.num_workers = 1 | ||||||
|  |  | ||||||
|         # instantiate all services here (TODO: inheritance?) |         # instantiate all services here (TODO: inheritance?) | ||||||
|  |         self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8) | ||||||
|         self.httpsrv = HttpSrv(self, None) |         self.httpsrv = HttpSrv(self, None) | ||||||
|  |         self.reload = self.noop | ||||||
|  |  | ||||||
|     def shutdown(self): |     def shutdown(self) -> None: | ||||||
|         # self.log("broker", "shutting down") |         # self.log("broker", "shutting down") | ||||||
|         self.httpsrv.shutdown() |         self.httpsrv.shutdown() | ||||||
|  |  | ||||||
|  |     def noop(self) -> None: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     def put(self, want_retval, dest, *args): |     def ask(self, dest: str, *args: Any) -> ExceptionalQueue: | ||||||
|  |  | ||||||
|  |         # new ipc invoking managed service in hub | ||||||
|  |         obj = self.hub | ||||||
|  |         for node in dest.split("."): | ||||||
|  |             obj = getattr(obj, node) | ||||||
|  |  | ||||||
|  |         rv = try_exec(True, obj, *args) | ||||||
|  |  | ||||||
|  |         # pretend we're broker_mp | ||||||
|  |         retq = ExceptionalQueue(1) | ||||||
|  |         retq.put(rv) | ||||||
|  |         return retq | ||||||
|  |  | ||||||
|  |     def say(self, dest: str, *args: Any) -> None: | ||||||
|         if dest == "listen": |         if dest == "listen": | ||||||
|             self.httpsrv.listen(args[0], 1) |             self.httpsrv.listen(args[0], 1) | ||||||
|  |             return | ||||||
|  |  | ||||||
|         else: |         if dest == "set_netdevs": | ||||||
|             # new ipc invoking managed service in hub |             self.httpsrv.set_netdevs(args[0]) | ||||||
|             obj = self.hub |             return | ||||||
|             for node in dest.split("."): |  | ||||||
|                 obj = getattr(obj, node) |  | ||||||
|  |  | ||||||
|             # TODO will deadlock if dest performs another ipc |         # new ipc invoking managed service in hub | ||||||
|             rv = try_exec(want_retval, obj, *args) |         obj = self.hub | ||||||
|             if not want_retval: |         for node in dest.split("."): | ||||||
|                 return |             obj = getattr(obj, node) | ||||||
|  |  | ||||||
|             # pretend we're broker_mp |         try_exec(False, obj, *args) | ||||||
|             retq = ExceptionalQueue(1) |  | ||||||
|             retq.put(rv) |  | ||||||
|             return retq |  | ||||||
|   | |||||||
| @@ -1,17 +1,28 @@ | |||||||
| # coding: utf-8 | # coding: utf-8 | ||||||
| from __future__ import print_function, unicode_literals | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
|  | import argparse | ||||||
| import traceback | import traceback | ||||||
|  |  | ||||||
| from .util import Pebkac, Queue | from queue import Queue | ||||||
|  |  | ||||||
|  | from .__init__ import TYPE_CHECKING | ||||||
|  | from .authsrv import AuthSrv | ||||||
|  | from .util import HMaccas, Pebkac | ||||||
|  |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from typing import Any, Optional, Union | ||||||
|  |  | ||||||
|  |     from .util import RootLogger | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from .httpsrv import HttpSrv | ||||||
|  |  | ||||||
|  |  | ||||||
| class ExceptionalQueue(Queue, object): | class ExceptionalQueue(Queue, object): | ||||||
|     def get(self, block=True, timeout=None): |     def get(self, block: bool = True, timeout: Optional[float] = None) -> Any: | ||||||
|         rv = super(ExceptionalQueue, self).get(block, timeout) |         rv = super(ExceptionalQueue, self).get(block, timeout) | ||||||
|  |  | ||||||
|         # TODO: how expensive is this? |  | ||||||
|         if isinstance(rv, list): |         if isinstance(rv, list): | ||||||
|             if rv[0] == "exception": |             if rv[0] == "exception": | ||||||
|                 if rv[1] == "pebkac": |                 if rv[1] == "pebkac": | ||||||
| @@ -22,7 +33,29 @@ class ExceptionalQueue(Queue, object): | |||||||
|         return rv |         return rv | ||||||
|  |  | ||||||
|  |  | ||||||
| def try_exec(want_retval, func, *args): | class BrokerCli(object): | ||||||
|  |     """ | ||||||
|  |     helps mypy understand httpsrv.broker but still fails a few levels deeper, | ||||||
|  |     for example resolving httpconn.* in httpcli -- see lines tagged #mypy404 | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     log: "RootLogger" | ||||||
|  |     args: argparse.Namespace | ||||||
|  |     asrv: AuthSrv | ||||||
|  |     httpsrv: "HttpSrv" | ||||||
|  |     iphash: HMaccas | ||||||
|  |  | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def ask(self, dest: str, *args: Any) -> ExceptionalQueue: | ||||||
|  |         return ExceptionalQueue(1) | ||||||
|  |  | ||||||
|  |     def say(self, dest: str, *args: Any) -> None: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def try_exec(want_retval: Union[bool, int], func: Any, *args: list[Any]) -> Any: | ||||||
|     try: |     try: | ||||||
|         return func(*args) |         return func(*args) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										226
									
								
								copyparty/cert.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								copyparty/cert.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | |||||||
|  | import calendar | ||||||
|  | import errno | ||||||
|  | import filecmp | ||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | from .util import Netdev, runcmd | ||||||
|  |  | ||||||
|  | HAVE_CFSSL = True | ||||||
|  |  | ||||||
|  | if True:  # pylint: disable=using-constant-test | ||||||
|  |     from .util import RootLogger | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def ensure_cert(log: "RootLogger", args) -> None: | ||||||
|  |     """ | ||||||
|  |     the default cert (and the entire TLS support) is only here to enable the | ||||||
|  |     crypto.subtle javascript API, which is necessary due to the webkit guys | ||||||
|  |     being massive memers (https://www.chromium.org/blink/webcrypto) | ||||||
|  |  | ||||||
|  |     i feel awful about this and so should they | ||||||
|  |     """ | ||||||
|  |     cert_insec = os.path.join(args.E.mod, "res/insecure.pem") | ||||||
|  |     cert_appdata = os.path.join(args.E.cfg, "cert.pem") | ||||||
|  |     if not os.path.isfile(args.cert): | ||||||
|  |         if cert_appdata != args.cert: | ||||||
|  |             raise Exception("certificate file does not exist: " + args.cert) | ||||||
|  |  | ||||||
|  |         shutil.copy(cert_insec, args.cert) | ||||||
|  |  | ||||||
|  |     with open(args.cert, "rb") as f: | ||||||
|  |         buf = f.read() | ||||||
|  |         o1 = buf.find(b" PRIVATE KEY-") | ||||||
|  |         o2 = buf.find(b" CERTIFICATE-") | ||||||
|  |         m = "unsupported certificate format: " | ||||||
|  |         if o1 < 0: | ||||||
|  |             raise Exception(m + "no private key inside pem") | ||||||
|  |         if o2 < 0: | ||||||
|  |             raise Exception(m + "no server certificate inside pem") | ||||||
|  |         if o1 > o2: | ||||||
|  |             raise Exception(m + "private key must appear before server certificate") | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         if filecmp.cmp(args.cert, cert_insec): | ||||||
|  |             t = "using default TLS certificate; https will be insecure:\033[36m {}" | ||||||
|  |             log("cert", t.format(args.cert), 3) | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     # speaking of the default cert, | ||||||
|  |     # printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _read_crt(args, fn): | ||||||
|  |     try: | ||||||
|  |         if not os.path.exists(os.path.join(args.crt_dir, fn)): | ||||||
|  |             return 0, {} | ||||||
|  |  | ||||||
|  |         acmd = ["cfssl-certinfo", "-cert", fn] | ||||||
|  |         rc, so, se = runcmd(acmd, cwd=args.crt_dir) | ||||||
|  |         if rc: | ||||||
|  |             return 0, {} | ||||||
|  |  | ||||||
|  |         inf = json.loads(so) | ||||||
|  |         zs = inf["not_after"] | ||||||
|  |         expiry = calendar.timegm(time.strptime(zs, "%Y-%m-%dT%H:%M:%SZ")) | ||||||
|  |         return expiry, inf | ||||||
|  |     except OSError as ex: | ||||||
|  |         if ex.errno == errno.ENOENT: | ||||||
|  |             raise | ||||||
|  |         return 0, {} | ||||||
|  |     except: | ||||||
|  |         return 0, {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _gen_ca(log: "RootLogger", args): | ||||||
|  |     expiry = _read_crt(args, "ca.pem")[0] | ||||||
|  |     if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry: | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     backdate = "{}m".format(int(args.crt_back * 60)) | ||||||
|  |     expiry = "{}m".format(int(args.crt_cdays * 60 * 24)) | ||||||
|  |     cn = args.crt_cnc.replace("--crt-cn", args.crt_cn) | ||||||
|  |     algo, ksz = args.crt_alg.split("-") | ||||||
|  |     req = { | ||||||
|  |         "CN": cn, | ||||||
|  |         "CA": {"backdate": backdate, "expiry": expiry, "pathlen": 0}, | ||||||
|  |         "key": {"algo": algo, "size": int(ksz)}, | ||||||
|  |         "names": [{"O": cn}], | ||||||
|  |     } | ||||||
|  |     sin = json.dumps(req).encode("utf-8") | ||||||
|  |     log("cert", "creating new ca ...", 6) | ||||||
|  |  | ||||||
|  |     cmd = "cfssl gencert -initca -" | ||||||
|  |     rc, so, se = runcmd(cmd.split(), 30, sin=sin) | ||||||
|  |     if rc: | ||||||
|  |         raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3) | ||||||
|  |  | ||||||
|  |     cmd = "cfssljson -bare ca" | ||||||
|  |     sin = so.encode("utf-8") | ||||||
|  |     rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir) | ||||||
|  |     if rc: | ||||||
|  |         raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3) | ||||||
|  |  | ||||||
|  |     bname = os.path.join(args.crt_dir, "ca") | ||||||
|  |     os.rename(bname + "-key.pem", bname + ".key") | ||||||
|  |     os.unlink(bname + ".csr") | ||||||
|  |  | ||||||
|  |     log("cert", "new ca OK", 2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]): | ||||||
|  |     names = args.crt_ns.split(",") if args.crt_ns else [] | ||||||
|  |     if not args.crt_exact: | ||||||
|  |         for n in names[:]: | ||||||
|  |             names.append("*.{}".format(n)) | ||||||
|  |     if not args.crt_noip: | ||||||
|  |         for ip in netdevs.keys(): | ||||||
|  |             names.append(ip.split("/")[0]) | ||||||
|  |     if args.crt_nolo: | ||||||
|  |         names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")] | ||||||
|  |     if not args.crt_nohn: | ||||||
|  |         names.append(args.name) | ||||||
|  |         names.append(args.name + ".local") | ||||||
|  |     if not names: | ||||||
|  |         names = ["127.0.0.1"] | ||||||
|  |     if "127.0.0.1" in names or "::1" in names: | ||||||
|  |         names.append("localhost") | ||||||
|  |     names = list({x: 1 for x in names}.keys()) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         expiry, inf = _read_crt(args, "srv.pem") | ||||||
|  |         expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.1 > expiry | ||||||
|  |         cert_insec = os.path.join(args.E.mod, "res/insecure.pem") | ||||||
|  |         for n in names: | ||||||
|  |             if n not in inf["sans"]: | ||||||
|  |                 raise Exception("does not have {}".format(n)) | ||||||
|  |         if expired: | ||||||
|  |             raise Exception("old server-cert has expired") | ||||||
|  |         if not filecmp.cmp(args.cert, cert_insec): | ||||||
|  |             return | ||||||
|  |     except Exception as ex: | ||||||
|  |         log("cert", "will create new server-cert; {}".format(ex)) | ||||||
|  |  | ||||||
|  |     log("cert", "creating server-cert ...", 6) | ||||||
|  |  | ||||||
|  |     backdate = "{}m".format(int(args.crt_back * 60)) | ||||||
|  |     expiry = "{}m".format(int(args.crt_sdays * 60 * 24)) | ||||||
|  |     cfg = { | ||||||
|  |         "signing": { | ||||||
|  |             "default": { | ||||||
|  |                 "backdate": backdate, | ||||||
|  |                 "expiry": expiry, | ||||||
|  |                 "usages": ["signing", "key encipherment", "server auth"], | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     with open(os.path.join(args.crt_dir, "cfssl.json"), "wb") as f: | ||||||
|  |         f.write(json.dumps(cfg).encode("utf-8")) | ||||||
|  |  | ||||||
|  |     cn = args.crt_cns.replace("--crt-cn", args.crt_cn) | ||||||
|  |     algo, ksz = args.crt_alg.split("-") | ||||||
|  |     req = { | ||||||
|  |         "key": {"algo": algo, "size": int(ksz)}, | ||||||
|  |         "names": [{"O": cn}], | ||||||
|  |     } | ||||||
|  |     sin = json.dumps(req).encode("utf-8") | ||||||
|  |  | ||||||
|  |     cmd = "cfssl gencert -config=cfssl.json -ca ca.pem -ca-key ca.key -profile=www" | ||||||
|  |     acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"] | ||||||
|  |     rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir) | ||||||
|  |     if rc: | ||||||
|  |         raise Exception("failed to create cert: {}, {}".format(rc, se)) | ||||||
|  |  | ||||||
|  |     cmd = "cfssljson -bare srv" | ||||||
|  |     sin = so.encode("utf-8") | ||||||
|  |     rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir) | ||||||
|  |     if rc: | ||||||
|  |         raise Exception("failed to translate cert: {}, {}".format(rc, se)) | ||||||
|  |  | ||||||
|  |     bname = os.path.join(args.crt_dir, "srv") | ||||||
|  |     try: | ||||||
|  |         os.unlink(bname + ".key") | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  |     os.rename(bname + "-key.pem", bname + ".key") | ||||||
|  |     os.unlink(bname + ".csr") | ||||||
|  |  | ||||||
|  |     with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f: | ||||||
|  |         ca = f.read() | ||||||
|  |  | ||||||
|  |     with open(bname + ".key", "rb") as f: | ||||||
|  |         skey = f.read() | ||||||
|  |  | ||||||
|  |     with open(bname + ".pem", "rb") as f: | ||||||
|  |         scrt = f.read() | ||||||
|  |  | ||||||
|  |     with open(args.cert, "wb") as f: | ||||||
|  |         f.write(skey + scrt + ca) | ||||||
|  |  | ||||||
|  |     log("cert", "new server-cert OK", 2) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]): | ||||||
|  |     global HAVE_CFSSL | ||||||
|  |  | ||||||
|  |     if args.http_only: | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     if args.no_crt or not HAVE_CFSSL: | ||||||
|  |         ensure_cert(log, args) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         _gen_ca(log, args) | ||||||
|  |         _gen_srv(log, args, netdevs) | ||||||
|  |     except Exception as ex: | ||||||
|  |         HAVE_CFSSL = False | ||||||
|  |         log("cert", "could not create TLS certificates: {}".format(ex), 3) | ||||||
|  |         if getattr(ex, "errno", 0) == errno.ENOENT: | ||||||
|  |             t = "install cfssl if you want to fix this; https://github.com/cloudflare/cfssl/releases/latest  (cfssl, cfssljson, cfssl-certinfo)" | ||||||
|  |             log("cert", t, 6) | ||||||
|  |  | ||||||
|  |         ensure_cert(log, args) | ||||||
							
								
								
									
										172
									
								
								copyparty/cfg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								copyparty/cfg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | # coding: utf-8 | ||||||
|  | from __future__ import print_function, unicode_literals | ||||||
|  |  | ||||||
|  | # awk -F\" '/add_argument\("-[^-]/{print(substr($2,2))}' copyparty/__main__.py | sort | tr '\n' ' ' | ||||||
|  | zs = "a c e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vp e2vu ed emp i j lo mcr mte mth mtm mtp nb nc nid nih nw p q s ss sss v z zv" | ||||||
|  | onedash = set(zs.split()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def vf_bmap() -> dict[str, str]: | ||||||
|  |     """argv-to-volflag: simple bools""" | ||||||
|  |     ret = { | ||||||
|  |         "never_symlink": "neversymlink", | ||||||
|  |         "no_dedup": "copydupes", | ||||||
|  |         "no_dupe": "nodupe", | ||||||
|  |         "no_forget": "noforget", | ||||||
|  |         "th_no_crop": "nocrop", | ||||||
|  |         "dav_auth": "davauth", | ||||||
|  |         "dav_rt": "davrt", | ||||||
|  |     } | ||||||
|  |     for k in ( | ||||||
|  |         "dotsrch", | ||||||
|  |         "e2t", | ||||||
|  |         "e2ts", | ||||||
|  |         "e2tsr", | ||||||
|  |         "e2v", | ||||||
|  |         "e2vu", | ||||||
|  |         "e2vp", | ||||||
|  |         "grid", | ||||||
|  |         "hardlink", | ||||||
|  |         "magic", | ||||||
|  |         "no_sb_md", | ||||||
|  |         "no_sb_lg", | ||||||
|  |         "rand", | ||||||
|  |         "xdev", | ||||||
|  |         "xlink", | ||||||
|  |         "xvol", | ||||||
|  |     ): | ||||||
|  |         ret[k] = k | ||||||
|  |     return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def vf_vmap() -> dict[str, str]: | ||||||
|  |     """argv-to-volflag: simple values""" | ||||||
|  |     ret = {"th_convt": "convt", "th_size": "thsize"} | ||||||
|  |     for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "unlist"): | ||||||
|  |         ret[k] = k | ||||||
|  |     return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def vf_cmap() -> dict[str, str]: | ||||||
|  |     """argv-to-volflag: complex/lists""" | ||||||
|  |     ret = {} | ||||||
|  |     for k in ("html_head", "mte", "mth"): | ||||||
|  |         ret[k] = k | ||||||
|  |     return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | permdescs = { | ||||||
|  |     "r": "read; list folder contents, download files", | ||||||
|  |     "w": 'write; upload files; need "r" to see the uploads', | ||||||
|  |     "m": 'move; move files and folders; need "w" at destination', | ||||||
|  |     "d": "delete; permanently delete files and folders", | ||||||
|  |     "g": "get; download files, but cannot see folder contents", | ||||||
|  |     "G": 'upget; same as "g" but can see filekeys of their own uploads', | ||||||
|  |     "h": 'html; same as "g" but folders return their index.html', | ||||||
|  |     "a": "admin; can see uploader IPs, config-reload", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | flagcats = { | ||||||
|  |     "uploads, general": { | ||||||
|  |         "nodupe": "rejects existing files (instead of symlinking them)", | ||||||
|  |         "hardlink": "does dedup with hardlinks instead of symlinks", | ||||||
|  |         "neversymlink": "disables symlink fallback; full copy instead", | ||||||
|  |         "copydupes": "disables dedup, always saves full copies of dupes", | ||||||
|  |         "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files", | ||||||
|  |         "nosub": "forces all uploads into the top folder of the vfs", | ||||||
|  |         "magic": "enables filetype detection for nameless uploads", | ||||||
|  |         "gz": "allows server-side gzip of uploads with ?gz (also c,xz)", | ||||||
|  |         "pk": "forces server-side compression, optional arg: xz,9", | ||||||
|  |     }, | ||||||
|  |     "upload rules": { | ||||||
|  |         "maxn=250,600": "max 250 uploads over 15min", | ||||||
|  |         "maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)", | ||||||
|  |         "vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)", | ||||||
|  |         "vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)", | ||||||
|  |         "rand": "force randomized filenames, 9 chars long by default", | ||||||
|  |         "nrand=N": "randomized filenames are N chars long", | ||||||
|  |         "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", | ||||||
|  |         "df=1g": "ensure 1 GiB free disk space", | ||||||
|  |     }, | ||||||
|  |     "upload rotation\n(moves all uploads into the specified folder structure)": { | ||||||
|  |         "rotn=100,3": "3 levels of subfolders with 100 entries in each", | ||||||
|  |         "rotf=%Y-%m/%d-%H": "date-formatted organizing", | ||||||
|  |         "lifetime=3600": "uploads are deleted after 1 hour", | ||||||
|  |     }, | ||||||
|  |     "database, general": { | ||||||
|  |         "e2d": "enable database; makes files searchable + enables upload dedup", | ||||||
|  |         "e2ds": "scan writable folders for new files on startup; also sets -e2d", | ||||||
|  |         "e2dsa": "scans all folders for new files on startup; also sets -e2d", | ||||||
|  |         "e2t": "enable multimedia indexing; makes it possible to search for tags", | ||||||
|  |         "e2ts": "scan existing files for tags on startup; also sets -e2t", | ||||||
|  |         "e2tsa": "delete all metadata from DB (full rescan); also sets -e2ts", | ||||||
|  |         "d2ts": "disables metadata collection for existing files", | ||||||
|  |         "d2ds": "disables onboot indexing, overrides -e2ds*", | ||||||
|  |         "d2t": "disables metadata collection, overrides -e2t*", | ||||||
|  |         "d2v": "disables file verification, overrides -e2v*", | ||||||
|  |         "d2d": "disables all database stuff, overrides -e2*", | ||||||
|  |         "hist=/tmp/cdb": "puts thumbnails and indexes at that location", | ||||||
|  |         "scan=60": "scan for new files every 60sec, same as --re-maxage", | ||||||
|  |         "nohash=\\.iso$": "skips hashing file contents if path matches *.iso", | ||||||
|  |         "noidx=\\.iso$": "fully ignores the contents at paths matching *.iso", | ||||||
|  |         "noforget": "don't forget files when deleted from disk", | ||||||
|  |         "fat32": "avoid excessive reindexing on android sdcardfs", | ||||||
|  |         "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff", | ||||||
|  |         "xlink": "cross-volume dupe detection / linking", | ||||||
|  |         "xdev": "do not descend into other filesystems", | ||||||
|  |         "xvol": "do not follow symlinks leaving the volume root", | ||||||
|  |         "dotsrch": "show dotfiles in search results", | ||||||
|  |         "nodotsrch": "hide dotfiles in search results (default)", | ||||||
|  |     }, | ||||||
|  |     'database, audio tags\n"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...': { | ||||||
|  |         "mtp=.bpm=f,audio-bpm.py": 'uses the "audio-bpm.py" program to\ngenerate ".bpm" tags from uploads (f = overwrite tags)', | ||||||
|  |         "mtp=ahash,vhash=media-hash.py": "collects two tags at once", | ||||||
|  |     }, | ||||||
|  |     "thumbnails": { | ||||||
|  |         "dthumb": "disables all thumbnails", | ||||||
|  |         "dvthumb": "disables video thumbnails", | ||||||
|  |         "dathumb": "disables audio thumbnails (spectrograms)", | ||||||
|  |         "dithumb": "disables image thumbnails", | ||||||
|  |         "thsize": "thumbnail res; WxH", | ||||||
|  |         "nocrop": "disable center-cropping", | ||||||
|  |         "convt": "conversion timeout in seconds", | ||||||
|  |     }, | ||||||
|  |     "handlers\n(better explained in --help-handlers)": { | ||||||
|  |         "on404=PY": "handle 404s by executing PY file", | ||||||
|  |         "on403=PY": "handle 403s by executing PY file", | ||||||
|  |     }, | ||||||
|  |     "event hooks\n(better explained in --help-hooks)": { | ||||||
|  |         "xbu=CMD": "execute CMD before a file upload starts", | ||||||
|  |         "xau=CMD": "execute CMD after  a file upload finishes", | ||||||
|  |         "xiu=CMD": "execute CMD after  all uploads finish and volume is idle", | ||||||
|  |         "xbr=CMD": "execute CMD before a file rename/move", | ||||||
|  |         "xar=CMD": "execute CMD after  a file rename/move", | ||||||
|  |         "xbd=CMD": "execute CMD before a file delete", | ||||||
|  |         "xad=CMD": "execute CMD after  a file delete", | ||||||
|  |         "xm=CMD": "execute CMD on message", | ||||||
|  |         "xban=CMD": "execute CMD if someone gets banned", | ||||||
|  |     }, | ||||||
|  |     "client and ux": { | ||||||
|  |         "grid": "show grid/thumbnails by default", | ||||||
|  |         "unlist": "dont list files matching REGEX", | ||||||
|  |         "html_head=TXT": "includes TXT in the <head>", | ||||||
|  |         "robots": "allows indexing by search engines (default)", | ||||||
|  |         "norobots": "kindly asks search engines to leave", | ||||||
|  |         "no_sb_md": "disable js sandbox for markdown files", | ||||||
|  |         "no_sb_lg": "disable js sandbox for prologue/epilogue", | ||||||
|  |         "sb_md": "enable js sandbox for markdown files (default)", | ||||||
|  |         "sb_lg": "enable js sandbox for prologue/epilogue (default)", | ||||||
|  |         "md_sbf": "list of markdown-sandbox safeguards to disable", | ||||||
|  |         "lg_sbf": "list of *logue-sandbox safeguards to disable", | ||||||
|  |         "nohtml": "return html and markdown as text/html", | ||||||
|  |     }, | ||||||
|  |     "others": { | ||||||
|  |         "fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission', | ||||||
|  |         "davauth": "ask webdav clients to login for all folders", | ||||||
|  |         "davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)", | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | flagdescs = {k.split("=")[0]: v for tab in flagcats.values() for k, v in tab.items()} | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user