mirror of
				https://github.com/9001/copyparty.git
				synced 2025-10-31 12:03:32 +00:00 
			
		
		
		
	Compare commits
	
		
			791 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 400d700845 | ||
|  | 82ce6862ee | ||
|  | 38e4fdfe03 | ||
|  | c04662798d | ||
|  | 19d156ff4e | ||
|  | 87c60a1ec9 | ||
|  | 2c92dab165 | ||
|  | 5c1e23907d | ||
|  | 925c7f0a57 | ||
|  | feed08deb2 | ||
|  | 560d7b6672 | ||
|  | 565daee98b | ||
|  | e396c5c2b5 | ||
|  | 1ee2cdd089 | ||
|  | beacedab50 | ||
|  | 25139a4358 | ||
|  | f8491970fd | ||
|  | da091aec85 | ||
|  | e9eb5affcd | ||
|  | c1918bc36c | ||
|  | fdda567f50 | ||
|  | 603d0ed72b | ||
|  | b15a4ef79f | ||
|  | 48a6789d36 | ||
|  | 36f2c446af | ||
|  | 69517e4624 | ||
|  | ea270ab9f2 | ||
|  | b6cf2d3089 | ||
|  | e8db3dd37f | ||
|  | 27485a4cb1 | ||
|  | 253a414443 | ||
|  | f6e693f0f5 | ||
|  | c5f7cfc355 | ||
|  | bc2c1e427a | ||
|  | 95d9e693c6 | ||
|  | 70a3cf36d1 | ||
|  | aa45fccf11 | ||
|  | 42d00050c1 | ||
|  | 4bb0e6e75a | ||
|  | 2f7f9de3f5 | ||
|  | f31ac90932 | ||
|  | 439cb7f85b | ||
|  | af193ee834 | ||
|  | c06126cc9d | ||
|  | 897ffbbbd0 | ||
|  | 8244d3b4fc | ||
|  | 74266af6d1 | ||
|  | 8c552f1ad1 | ||
|  | bf5850785f | ||
|  | feecb3e0b8 | ||
|  | 08d8c82167 | ||
|  | 5239e7ac0c | ||
|  | 9937c2e755 | ||
|  | f1e947f37d | ||
|  | a70a49b9c9 | ||
|  | fe700dcf1a | ||
|  | c8e3ed3aae | ||
|  | b8733653a3 | ||
|  | b772a4f8bb | ||
|  | 9e5253ef87 | ||
|  | 7b94e4edf3 | ||
|  | da26ec36ca | ||
|  | 443acf2f8b | ||
|  | 6c90e3893d | ||
|  | ea002ee71d | ||
|  | ab18893cd2 | ||
|  | 844d16b9e5 | ||
|  | 989cc613ef | ||
|  | 4f0cad5468 | ||
|  | f89de6b35d | ||
|  | e0bcb88ee7 | ||
|  | a0022805d1 | ||
|  | 853adb5d04 | ||
|  | 7744226b5c | ||
|  | d94b5b3fc9 | ||
|  | e6ba065bc2 | ||
|  | 59a53ba9ac | ||
|  | b88cc7b5ce | ||
|  | 5ab54763c6 | ||
|  | 59f815ff8c | ||
|  | 9c42cbec6f | ||
|  | f471b05aa4 | ||
|  | 34c32e3e89 | ||
|  | a080759a03 | ||
|  | 0ae12868e5 | ||
|  | ef52e2c06c | ||
|  | 32c912bb16 | ||
|  | 20870fda79 | ||
|  | bdfe2c1a5f | ||
|  | cb99fbf442 | ||
|  | bccc44dc21 | ||
|  | 2f20d29edd | ||
|  | c6acd3a904 | ||
|  | 2b24c50eb7 | ||
|  | d30ae8453d | ||
|  | 8e5c436bef | ||
|  | f500e55e68 | ||
|  | 9700a12366 | ||
|  | 2b6a34dc5c | ||
|  | ee80cdb9cf | ||
|  | 2def4cd248 | ||
|  | 0287c7baa5 | ||
|  | 51d31588e6 | ||
|  | 32553e4520 | ||
|  | 211a30da38 | ||
|  | bdbcbbb002 | ||
|  | e78af02241 | ||
|  | 115020ba60 | ||
|  | 66abf17bae | ||
|  | b377791be7 | ||
|  | 78919e65d6 | ||
|  | 84b52ea8c5 | ||
|  | fd89f7ecb9 | ||
|  | 2ebfdc2562 | ||
|  | dbf1cbc8af | ||
|  | a259704596 | ||
|  | 04b55f1a1d | ||
|  | 206af8f151 | ||
|  | 645bb5c990 | ||
|  | f8966222e4 | ||
|  | d71f844b43 | ||
|  | e8b7f65f82 | ||
|  | f193f398c1 | ||
|  | b6554a7f8c | ||
|  | 3f05b6655c | ||
|  | 51a83b04a0 | ||
|  | 0c03921965 | ||
|  | 2527e90325 | ||
|  | 7f08f10c37 | ||
|  | 1c011ff0bb | ||
|  | a1ad608267 | ||
|  | 547a486387 | ||
|  | 7741870dc7 | ||
|  | 8785d2f9fe | ||
|  | d744f3ff8f | ||
|  | 8ca996e2f7 | ||
|  | 096de50889 | ||
|  | bec3fee9ee | ||
|  | 8413ed6d1f | ||
|  | 055302b5be | ||
|  | 8016e6711b | ||
|  | c8ea4066b1 | ||
|  | 6cc7101d31 | ||
|  | 263adec70a | ||
|  | ac96fd9c96 | ||
|  | e5582605cd | ||
|  | 1b52ef1f8a | ||
|  | 503face974 | ||
|  | 13e77777d7 | ||
|  | 89c6c2e0d9 | ||
|  | 14af136fcd | ||
|  | d39a99c929 | ||
|  | 43ee6b9f5b | ||
|  | 8a38101e48 | ||
|  | 5026b21226 | ||
|  | d07859e8e6 | ||
|  | df7219d3b6 | ||
|  | ad9be54f55 | ||
|  | eeecc50757 | ||
|  | 8ff7094e4d | ||
|  | 58ae38c613 | ||
|  | 7f1c992601 | ||
|  | fbfdd8338b | ||
|  | bbc379906a | ||
|  | 33f41f3e61 | ||
|  | 655f6d00f8 | ||
|  | fd552842d4 | ||
|  | 6bd087ddc5 | ||
|  | 0504b010a1 | ||
|  | 39cc92d4bc | ||
|  | a0da0122b9 | ||
|  | 879e83e24f | ||
|  | 64ad585318 | ||
|  | f262aee800 | ||
|  | d4da386172 | ||
|  | 5d92f4df49 | ||
|  | 6f8a588c4d | ||
|  | 7c8e368721 | ||
|  | f7a43a8e46 | ||
|  | 02879713a2 | ||
|  | acbb8267e1 | ||
|  | 8796c09f56 | ||
|  | d636316a19 | ||
|  | a96d9ac6cb | ||
|  | 643e222986 | ||
|  | ed524d84bb | ||
|  | f0cdd9f25d | ||
|  | 4e797a7156 | ||
|  | 136c0fdc2b | ||
|  | 35165f8472 | ||
|  | cab999978e | ||
|  | fabeebd96b | ||
|  | b1cf588452 | ||
|  | c354a38b4c | ||
|  | a17c267d87 | ||
|  | c1180d6f9c | ||
|  | d3db6d296f | ||
|  | caf7e93f5e | ||
|  | eefa0518db | ||
|  | 945170e271 | ||
|  | 6c2c6090dc | ||
|  | b2e233403d | ||
|  | e397ec2e48 | ||
|  | fade751a3e | ||
|  | 0f386c4b08 | ||
|  | 14bccbe45f | ||
|  | 55eb692134 | ||
|  | b32d65207b | ||
|  | 64cac003d8 | ||
|  | 6dbfcddcda | ||
|  | b4e0a34193 | ||
|  | 01c82b54a7 | ||
|  | 4ef3106009 | ||
|  | aa3a971961 | ||
|  | b9d0c8536b | ||
|  | 3313503ea5 | ||
|  | d999d3a921 | ||
|  | e7d00bae39 | ||
|  | 650e41c717 | ||
|  | 140f6e0389 | ||
|  | 5e111ba5ee | ||
|  | 95a599961e | ||
|  | a55e0d6eb8 | ||
|  | 2fd2c6b948 | ||
|  | 7a936ea01e | ||
|  | 226c7c3045 | ||
|  | a4239a466b | ||
|  | d0eb014c38 | ||
|  | e01ba8552a | ||
|  | 024303592a | ||
|  | 86419b8f47 | ||
|  | f1358dbaba | ||
|  | e8a653ca0c | ||
|  | 9bc09ce949 | ||
|  | dc8e621d7c | ||
|  | dee0950f74 | ||
|  | 143f72fe36 | ||
|  | a7889fb6a2 | ||
|  | 987caec15d | ||
|  | ab40ff5051 | ||
|  | bed133d3dd | ||
|  | 829c8fca96 | ||
|  | 5b26ab0096 | ||
|  | 39554b4bc3 | ||
|  | 97d9c149f1 | ||
|  | 59688bc8d7 | ||
|  | a18f63895f | ||
|  | 27433d6214 | ||
|  | 374c535cfa | ||
|  | ac7815a0ae | ||
|  | 10bc2d9205 | ||
|  | 0c50ea1757 | ||
|  | c057c5e8e8 | ||
|  | 46d667716e | ||
|  | cba2e10d29 | ||
|  | b1693f95cb | ||
|  | 3f00073256 | ||
|  | d15000062d | ||
|  | 6cb3b35a54 | ||
|  | b4031e8d43 | ||
|  | a3ca0638cb | ||
|  | a360ac29da | ||
|  | 9672b8c9b3 | ||
|  | e70ecd98ef | ||
|  | 5f7ce78d7f | ||
|  | 2077dca66f | ||
|  | 91f010290c | ||
|  | 395e3386b7 | ||
|  | a1dce0f24e | ||
|  | c7770904e6 | ||
|  | 1690889ed8 | ||
|  | 842817d9e3 | ||
|  | 5fc04152bd | ||
|  | 1be85bdb26 | ||
|  | 2eafaa88a2 | ||
|  | 900cc463c3 | ||
|  | 97b999c463 | ||
|  | a7cef91b8b | ||
|  | a4a112c0ee | ||
|  | e6bcee28d6 | ||
|  | 626b5770a5 | ||
|  | c2f92cacc1 | ||
|  | 4f8a1f5f6a | ||
|  | 4a98b73915 | ||
|  | 00812cb1da | ||
|  | 16766e702e | ||
|  | 5e932a9504 | ||
|  | ccab44daf2 | ||
|  | 8c52b88767 | ||
|  | c9fd26255b | ||
|  | 0b9b8dbe72 | ||
|  | b7723ac245 | ||
|  | 35b75c3db1 | ||
|  | f902779050 | ||
|  | fdddd36a5d | ||
|  | c4ba123779 | ||
|  | 72e355eb2c | ||
|  | 43d409a5d9 | ||
|  | b1fffc2246 | ||
|  | edd3e53ab3 | ||
|  | aa0b119031 | ||
|  | eddce00765 | ||
|  | 6f4bde2111 | ||
|  | f3035e8869 | ||
|  | a9730499c0 | ||
|  | b66843efe2 | ||
|  | cc1aaea300 | ||
|  | 9ccc238799 | ||
|  | 8526ef9368 | ||
|  | 3c36727d07 | ||
|  | ef33ce94cd | ||
|  | d500baf5c5 | ||
|  | deef32335e | ||
|  | fc4b51ad00 | ||
|  | fa762754bf | ||
|  | 29bd8f57c4 | ||
|  | abc37354ef | ||
|  | ee3333362f | ||
|  | 7c0c6b94a3 | ||
|  | bac733113c | ||
|  | 32ab65d7cb | ||
|  | c6744dc483 | ||
|  | b9997d677d | ||
|  | 10defe6aef | ||
|  | 736aa125a8 | ||
|  | eb48373b8b | ||
|  | d4a7b7d84d | ||
|  | 2923a38b87 | ||
|  | dabdaaee33 | ||
|  | 65e4d67c3e | ||
|  | 4b720f4150 | ||
|  | 2e85a25614 | ||
|  | 713fffcb8e | ||
|  | 8020b11ea0 | ||
|  | 2523d76756 | ||
|  | 7ede509973 | ||
|  | 7c1d97af3b | ||
|  | 95566e8388 | ||
|  | 76afb62b7b | ||
|  | 7dec922c70 | ||
|  | c07e0110f8 | ||
|  | 2808734047 | ||
|  | 1f75314463 | ||
|  | 063fa3efde | ||
|  | 44693d79ec | ||
|  | cea746377e | ||
|  | 59a98bd2b5 | ||
|  | 250aa28185 | ||
|  | 5280792cd7 | ||
|  | 2529aa151d | ||
|  | fc658e5b9e | ||
|  | a4bad62b60 | ||
|  | e1d78d8b23 | ||
|  | c7f826dbbe | ||
|  | 801da8079b | ||
|  | 7d797dba3f | ||
|  | cda90c285e | ||
|  | 4b5a0787ab | ||
|  | 2048b7538e | ||
|  | ac40dccc8f | ||
|  | 9ca8154651 | ||
|  | db668ba491 | ||
|  | edbafd94c2 | ||
|  | 2df76eb6e1 | ||
|  | 9b77c9ce7d | ||
|  | dc2b67f155 | ||
|  | 9f32e9e11d | ||
|  | 7086d2a305 | ||
|  | 575615ca2d | ||
|  | c0da4b09bf | ||
|  | 22880ccc9a | ||
|  | e4001550c1 | ||
|  | e9f65be86a | ||
|  | 3b9919a486 | ||
|  | acc363133f | ||
|  | 8f2d502d4d | ||
|  | 2ae93ad715 | ||
|  | bb590e364a | ||
|  | e7fff77735 | ||
|  | 753e3cfbaf | ||
|  | 99e9cba1f7 | ||
|  | fcc3336760 | ||
|  | 0dc3c23b42 | ||
|  | 6aa10ecedc | ||
|  | 93125bba4d | ||
|  | fae5a36e6f | ||
|  | fc9b729fc2 | ||
|  | 8620ae5bb7 | ||
|  | 01a851da28 | ||
|  | 309895d39d | ||
|  | 7ac0803ded | ||
|  | 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 | 
							
								
								
									
										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/   | ||||
							
								
								
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -12,6 +12,7 @@ copyparty.egg-info/ | ||||
| /dist/ | ||||
| /py2/ | ||||
| /sfx* | ||||
| /pyz/ | ||||
| /unt/ | ||||
| /log/ | ||||
|  | ||||
| @@ -21,11 +22,23 @@ copyparty.egg-info/ | ||||
| # winmerge | ||||
| *.bak | ||||
|  | ||||
| # apple pls | ||||
| .DS_Store | ||||
|  | ||||
| # derived | ||||
| copyparty/res/COPYING.txt | ||||
| copyparty/web/deps/ | ||||
| srv/ | ||||
| scripts/docker/i/ | ||||
| contrib/package/arch/pkg/ | ||||
| contrib/package/arch/src/ | ||||
|  | ||||
| # state/logs | ||||
| up.*.txt | ||||
| .hist/ | ||||
| .hist/ | ||||
| scripts/docker/*.out | ||||
| scripts/docker/*.err | ||||
| /perf.* | ||||
|  | ||||
| # nix build output link | ||||
| result | ||||
|   | ||||
							
								
								
									
										7
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -9,14 +9,17 @@ | ||||
|             "console": "integratedTerminal", | ||||
|             "cwd": "${workspaceFolder}", | ||||
|             "justMyCode": false, | ||||
|             "env": { | ||||
|                 "PYDEVD_DISABLE_FILE_VALIDATION": "1", | ||||
|                 "PYTHONWARNINGS": "always", //error | ||||
|             }, | ||||
|             "args": [ | ||||
|                 //"-nw", | ||||
|                 "-ed", | ||||
|                 "-emp", | ||||
|                 "-e2dsa", | ||||
|                 "-e2ts", | ||||
|                 "-mtp", | ||||
|                 ".bpm=f,bin/mtag/audio-bpm.py", | ||||
|                 "-mtp=.bpm=f,bin/mtag/audio-bpm.py", | ||||
|                 "-aed:wark", | ||||
|                 "-vsrv::r:rw,ed:c,dupe", | ||||
|                 "-vdist:dist:r" | ||||
|   | ||||
							
								
								
									
										12
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							| @@ -30,10 +30,18 @@ except: | ||||
|  | ||||
| argv = [os.path.expanduser(x) if x.startswith("~") else x for x in 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 re.search(" -j ?[0-9]", " ".join(argv)): | ||||
|     argv = [sys.executable, "-m", "copyparty"] + argv | ||||
| if sfx: | ||||
|     argv = [sys.executable, sfx] + argv | ||||
|     sp.check_call(argv) | ||||
| elif re.search(" -j ?[0-9]", " ".join(argv)): | ||||
|     argv = [sys.executable, "-Wa", "-m", "copyparty"] + argv | ||||
|     sp.check_call(argv) | ||||
| else: | ||||
|     sys.path.insert(0, os.getcwd()) | ||||
|   | ||||
							
								
								
									
										32
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -35,34 +35,18 @@ | ||||
|     "python.linting.flake8Enabled": true, | ||||
|     "python.linting.banditEnabled": true, | ||||
|     "python.linting.mypyEnabled": true, | ||||
|     "python.linting.mypyArgs": [ | ||||
|         "--ignore-missing-imports", | ||||
|         "--follow-imports=silent", | ||||
|         "--show-column-numbers", | ||||
|         "--strict" | ||||
|     ], | ||||
|     "python.linting.flake8Args": [ | ||||
|         "--max-line-length=120", | ||||
|         "--ignore=E722,F405,E203,W503,W293,E402,E501,E128", | ||||
|         "--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226", | ||||
|     ], | ||||
|     "python.linting.banditArgs": [ | ||||
|         "--ignore=B104" | ||||
|     ], | ||||
|     "python.linting.pylintArgs": [ | ||||
|         "--disable=missing-module-docstring", | ||||
|         "--disable=missing-class-docstring", | ||||
|         "--disable=missing-function-docstring", | ||||
|         "--disable=import-outside-toplevel", | ||||
|         "--disable=wrong-import-position", | ||||
|         "--disable=raise-missing-from", | ||||
|         "--disable=bare-except", | ||||
|         "--disable=broad-except", | ||||
|         "--disable=invalid-name", | ||||
|         "--disable=line-too-long", | ||||
|         "--disable=consider-using-f-string" | ||||
|         "--ignore=B104,B110,B112" | ||||
|     ], | ||||
|     // python3 -m isort --py=27 --profile=black copyparty/ | ||||
|     "python.formatting.provider": "black", | ||||
|     "python.formatting.provider": "none", | ||||
|     "[python]": { | ||||
|         "editor.defaultFormatter": "ms-python.black-formatter" | ||||
|     }, | ||||
|     "editor.formatOnSave": true, | ||||
|     "[html]": { | ||||
|         "editor.formatOnSave": false, | ||||
| @@ -74,10 +58,6 @@ | ||||
|     "files.associations": { | ||||
|         "*.makefile": "makefile" | ||||
|     }, | ||||
|     "python.formatting.blackArgs": [ | ||||
|         "-t", | ||||
|         "py27" | ||||
|     ], | ||||
|     "python.linting.enabled": true, | ||||
|     "python.pythonPath": "/usr/bin/python3" | ||||
| } | ||||
							
								
								
									
										1
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -11,6 +11,7 @@ | ||||
|             "type": "shell", | ||||
|             "command": "${config:python.pythonPath}", | ||||
|             "args": [ | ||||
|                 "-Wa", //-We | ||||
|                 ".vscode/launch.py" | ||||
|             ] | ||||
|         } | ||||
|   | ||||
| @@ -1,3 +1,43 @@ | ||||
| * do something cool | ||||
|  | ||||
| really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight | ||||
| really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight 👍👍 | ||||
|  | ||||
| but to be more specific, | ||||
|  | ||||
|  | ||||
| # contribution ideas | ||||
|  | ||||
|  | ||||
| ## documentation | ||||
|  | ||||
| I think we can agree that the documentation leaves a LOT to be desired. I've realized I'm not exactly qualified for this 😅 but maybe the [soon-to-come setup GUI](https://github.com/9001/copyparty/issues/57) will make this more manageable. The best documentation is the one that never had to be written, right? :> so I suppose we can give this a wait-and-see approach for a bit longer. | ||||
|  | ||||
|  | ||||
| ## crazy ideas & features | ||||
|  | ||||
| assuming they won't cause too much problems or side-effects :>  | ||||
|  | ||||
| i think someone was working on a way to list directories over DNS for example... | ||||
|  | ||||
| if you wanna have a go at coding it up yourself then maybe mention the idea on discord before you get too far, otherwise just go nuts 👍 | ||||
|  | ||||
|  | ||||
| ## others | ||||
|  | ||||
| aside from documentation and ideas, some other things that would be cool to have some help with is: | ||||
|  | ||||
| * **translations** -- the copyparty web-UI has translations for english and norwegian at the top of [browser.js](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.js); if you'd like to add a translation for another language then that'd be welcome! and if that language has a grammar that doesn't fit into the way the strings are assembled, then we'll fix that as we go :> | ||||
|  | ||||
| * **UI ideas** -- at some point I was thinking of rewriting the UI in react/preact/something-not-vanilla-javascript, but I'll admit the comfiness of not having any build stage combined with raw performance has kinda convinced me otherwise :p but I'd be very open to ideas on how the UI could be improved, or be more intuitive. | ||||
|  | ||||
| * **docker improvements** -- I don't really know what I'm doing when it comes to containers, so I'm sure there's a *huge* room for improvement here, mainly regarding how you're supposed to use the container with kubernetes / docker-compose / any of the other popular ways to do things. At some point I swear I'll start learning about docker so I can pick up clach04's [docker-compose draft](https://github.com/9001/copyparty/issues/38) and learn how that stuff ticks, unless someone beats me to it! | ||||
|  | ||||
| * **packaging** for various linux distributions -- this could either be as simple as just plopping the sfx.py in the right place and calling that from systemd (the archlinux package [originally did this](https://github.com/9001/copyparty/pull/18)); maybe with a small config-file which would cause copyparty to load settings from `/etc/copyparty.d` (like the [archlinux package](https://github.com/9001/copyparty/tree/hovudstraum/contrib/package/arch) does with `copyparty.conf`), or it could be a proper installation of the copyparty python package into /usr/lib or similar (the archlinux package [eventually went for this approach](https://github.com/9001/copyparty/pull/26)) | ||||
|  | ||||
|   * [fpm](https://github.com/jordansissel/fpm) can probably help with the technical part of it, but someone needs to handle distro relations :-) | ||||
|  | ||||
| * **software integration** -- I'm sure there's a lot of usecases where copyparty could complement something else, or the other way around, so any ideas or any work in this regard would be dope. This doesn't necessarily have to be code inside copyparty itself; | ||||
|  | ||||
|   * [hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) -- these are small programs which are called by copyparty when certain things happen (files are uploaded, someone hits a 404, etc.), and could be a fun way to add support for more usecases | ||||
|  | ||||
|   * [parser plugins](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) -- if you want to have copyparty analyze and index metadata for some oddball file-formats, then additional plugins would be neat :> | ||||
|   | ||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2019 ed | ||||
| Copyright (c) 2019 ed <oss@ocv.me> | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # [`up2k.py`](up2k.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 | ||||
|   | ||||
| @@ -207,7 +207,7 @@ def examples(): | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     global NC, BY_PATH | ||||
|     global NC, BY_PATH  # pylint: disable=global-statement | ||||
|     os.system("") | ||||
|     print() | ||||
|  | ||||
| @@ -282,7 +282,8 @@ def main(): | ||||
|         if ver == "corrupt": | ||||
|             die("{} database appears to be corrupt, sorry") | ||||
|  | ||||
|         if ver < DB_VER1 or ver > DB_VER2: | ||||
|         iver = int(ver) | ||||
|         if iver < DB_VER1 or iver > DB_VER2: | ||||
|             m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first" | ||||
|             die(m) | ||||
|  | ||||
|   | ||||
							
								
								
									
										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")) | ||||
| @@ -2,15 +2,25 @@ standalone programs which are executed by copyparty when an event happens (uploa | ||||
|  | ||||
| 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 | ||||
| * [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks | ||||
| * [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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										9
									
								
								bin/hooks/discord-announce.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										9
									
								
								bin/hooks/discord-announce.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -13,9 +13,15 @@ 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:c,xau=f,t5,j,bin/hooks/discord-announce.py | ||||
|     -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 | ||||
| @@ -30,6 +36,7 @@ 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]) | ||||
|   | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										123
									
								
								bin/hooks/msg-log.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										123
									
								
								bin/hooks/msg-log.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| #!/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 | ||||
|  | ||||
| try: | ||||
|     from datetime import datetime, timezone | ||||
| except: | ||||
|     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) | ||||
|  | ||||
|     try: | ||||
|         dt = datetime.fromtimestamp(msg_info["at"], timezone.utc) | ||||
|     except: | ||||
|         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() | ||||
							
								
								
									
										48
									
								
								bin/hooks/notify.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										48
									
								
								bin/hooks/notify.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,20 +1,28 @@ | ||||
| #!/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 | ||||
| show os notification on upload; works on windows, linux, macos, android | ||||
|  | ||||
| depdencies: | ||||
|     python3 -m pip install --user -U plyer | ||||
|     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 usage as global config: | ||||
| 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 | ||||
|                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
|  | ||||
| example usage as a volflag (per-volume config): | ||||
|     -v srv/inc:inc: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 | ||||
| @@ -22,8 +30,36 @@ parameters explained, | ||||
| """ | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from copyparty.util import humansize | ||||
| except: | ||||
|  | ||||
|     def humansize(n): | ||||
|         return n | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     notification.notify(title="new file uploaded", message=sys.argv[1], timeout=10) | ||||
|     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__": | ||||
|   | ||||
							
								
								
									
										73
									
								
								bin/hooks/notify2.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										73
									
								
								bin/hooks/notify2.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import json | ||||
| import os | ||||
| import sys | ||||
| import subprocess as sp | ||||
| from datetime import datetime, timezone | ||||
| 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) | ||||
|     dt = datetime.fromtimestamp(inf["mt"], timezone.utc) | ||||
|     mt = dt.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() | ||||
							
								
								
									
										7
									
								
								bin/hooks/reject-extension.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										7
									
								
								bin/hooks/reject-extension.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -10,7 +10,12 @@ example usage as global config: | ||||
|     --xbu c,bin/hooks/reject-extension.py | ||||
|  | ||||
| example usage as a volflag (per-volume config): | ||||
|     -v srv/inc:inc:c,xbu=c,bin/hooks/reject-extension.py | ||||
|     -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 | ||||
|   | ||||
							
								
								
									
										7
									
								
								bin/hooks/reject-mimetype.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										7
									
								
								bin/hooks/reject-mimetype.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -17,7 +17,12 @@ example usage as global config: | ||||
|     --xau c,bin/hooks/reject-mimetype.py | ||||
|  | ||||
| example usage as a volflag (per-volume config): | ||||
|     -v srv/inc:inc:c,xau=c,bin/hooks/reject-mimetype.py | ||||
|     -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 | ||||
|   | ||||
							
								
								
									
										12
									
								
								bin/hooks/wget.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										12
									
								
								bin/hooks/wget.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -15,9 +15,15 @@ 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:c,xm=f,j,t3600,bin/hooks/wget.py | ||||
|     -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 | ||||
| @@ -31,6 +37,10 @@ def main(): | ||||
|     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] | ||||
|   | ||||
							
								
								
									
										111
									
								
								bin/hooks/xiu-sha.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										111
									
								
								bin/hooks/xiu-sha.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import hashlib | ||||
| import json | ||||
| import sys | ||||
| from datetime import datetime, timezone | ||||
|  | ||||
|  | ||||
| _ = 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 | ||||
|  | ||||
|  | ||||
| UTC = timezone.utc | ||||
|  | ||||
|  | ||||
| def humantime(ts): | ||||
|     return datetime.fromtimestamp(ts, UTC).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.now(UTC).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() | ||||
| @@ -24,6 +24,15 @@ these do not have any problematic dependencies at all: | ||||
|   * 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 | ||||
|  | ||||
| run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos) | ||||
| @@ -31,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: | ||||
|  | ||||
| * from distro: `numpy vamp-plugin-sdk beatroot-vamp mixxx-keyfinder ffmpeg` | ||||
| * from pypy: `keyfinder vamp` | ||||
| * from pip: `keyfinder vamp` | ||||
|  | ||||
|  | ||||
| # usage from copyparty | ||||
|   | ||||
| @@ -16,6 +16,10 @@ dep: ffmpeg | ||||
| """ | ||||
|  | ||||
|  | ||||
| # save beat timestamps to ".beats/filename.txt" | ||||
| SAVE = False | ||||
|  | ||||
|  | ||||
| def det(tf): | ||||
|     # fmt: off | ||||
|     sp.check_call([ | ||||
| @@ -23,12 +27,11 @@ def det(tf): | ||||
|         b"-nostdin", | ||||
|         b"-hide_banner", | ||||
|         b"-v", b"fatal", | ||||
|         b"-ss", b"13", | ||||
|         b"-y", b"-i", fsenc(sys.argv[1]), | ||||
|         b"-map", b"0:a:0", | ||||
|         b"-ac", b"1", | ||||
|         b"-ar", b"22050", | ||||
|         b"-t", b"300", | ||||
|         b"-t", b"360", | ||||
|         b"-f", b"f32le", | ||||
|         fsenc(tf) | ||||
|     ]) | ||||
| @@ -47,10 +50,29 @@ def det(tf): | ||||
|             print(c["list"][0]["label"].split(" ")[0]) | ||||
|             return | ||||
|  | ||||
|         # throws if detection failed: | ||||
|         bpm = float(cl[-1]["timestamp"] - cl[1]["timestamp"]) | ||||
|         bpm = round(60 * ((len(cl) - 1) / bpm), 2) | ||||
|         print(f"{bpm:.2f}") | ||||
|     # throws if detection failed: | ||||
|     beats = [float(x["timestamp"]) for x in cl] | ||||
|     bds = [b - a for a, b in zip(beats, beats[1:])] | ||||
|     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(): | ||||
|   | ||||
| @@ -61,7 +61,7 @@ def main(): | ||||
|  | ||||
|     os.chdir(cwd) | ||||
|     f1 = fsenc(fn) | ||||
|     f2 = os.path.join(b"noexif", f1) | ||||
|     f2 = fsenc(os.path.join(b"noexif", fn)) | ||||
|     cmd = [ | ||||
|         b"exiftool", | ||||
|         b"-exif:all=", | ||||
|   | ||||
| @@ -7,6 +7,7 @@ set -e | ||||
| # 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 | ||||
| # linux/arch:   requires gcc make cmake patchelf python3 ffmpeg fftw libsndfile python-{numpy,wheel,pip,setuptools} | ||||
| # win64: requires msys2-mingw64 environment | ||||
| # macos: requires macports | ||||
| # | ||||
| @@ -57,6 +58,7 @@ hash -r | ||||
| 	command -v python3 && pybin=python3 || pybin=python | ||||
| } | ||||
|  | ||||
| $pybin -c 'import numpy' || | ||||
| $pybin -m pip install --user numpy | ||||
|  | ||||
|  | ||||
| @@ -221,27 +223,31 @@ install_vamp() { | ||||
| 	#   use msys2 in mingw-w64 mode | ||||
| 	#   pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk} | ||||
| 	 | ||||
| 	$pybin -m pip install --user vamp | ||||
| 	$pybin -m pip install --user vamp || { | ||||
| 		printf '\n\033[7malright, trying something else...\033[0m\n' | ||||
| 		$pybin -m pip install --user --no-build-isolation vamp | ||||
| 	} | ||||
|  | ||||
| 	cd "$td" | ||||
| 	echo '#include <vamp-sdk/Plugin.h>' | gcc -x c -c -o /dev/null - || [ -e ~/pe/vamp-sdk ] || { | ||||
| 	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) | ||||
| 		(dl_files yolo https://ocv.me/mirror/vamp-plugin-sdk-2.10.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 | ||||
| 			echo "153b7f2fa01b77c65ad393ca0689742d66421017fd5931d216caa0fcf6909355fff74706fabbc062a3a04588a619c9b515a1dae00f21a57afd97902a355c48ed  -" | ||||
| 		) <vamp-plugin-sdk-2.10.0.tar.gz | ||||
| 		tar -xf vamp-plugin-sdk-2.10.0.tar.gz | ||||
| 		rm -- *.tar.gz | ||||
| 		ls -al | ||||
| 		cd vamp-plugin-sdk-* | ||||
| 		./configure --prefix=$HOME/pe/vamp-sdk | ||||
| 		printf '%s\n' "int main(int argc, char **argv) { return 0; }" > host/vamp-simple-host.cpp | ||||
| 		./configure --disable-programs --prefix=$HOME/pe/vamp-sdk | ||||
| 		make -j1 install | ||||
| 	} | ||||
|  | ||||
| 	cd "$td" | ||||
| 	have_beatroot || { | ||||
| 		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://ocv.me/mirror/beatroot-vamp-v1.0.tar.gz) | ||||
| 		sha512sum -c <( | ||||
| 			echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874  -" | ||||
| 		) <beatroot-vamp-v1.0.tar.gz | ||||
| @@ -249,8 +255,9 @@ install_vamp() { | ||||
| 		rm -- *.tar.gz | ||||
| 		cd beatroot-vamp-v1.0 | ||||
| 		[ -e ~/pe/vamp-sdk ] && | ||||
| 			sed -ri 's`^(CFLAGS :=.*)`\1 -I'$HOME'/pe/vamp-sdk/include`' Makefile.linux | ||||
| 		make -f Makefile.linux -j4 LDFLAGS=-L$HOME/pe/vamp-sdk/lib | ||||
| 			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 | ||||
| 		mkdir ~/vamp | ||||
| 		cp -pv beatroot-vamp.* ~/vamp/ | ||||
|   | ||||
| @@ -1,6 +1,11 @@ | ||||
| #!/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 | ||||
| @@ -10,16 +15,17 @@ use copyparty as a chromecast replacement: | ||||
|  | ||||
| the android app makes it a breeze to post pics and links: | ||||
|   https://github.com/9001/party-up/releases | ||||
|   (iOS devices have to rely on the web-UI) | ||||
|  | ||||
| goes without saying, but this is HELLA DANGEROUS, | ||||
|   GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS | ||||
| iOS devices can use the web-UI or the shortcut instead: | ||||
|   https://github.com/9001/copyparty#ios-shortcuts | ||||
|  | ||||
| example copyparty config to use this: | ||||
|   --urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py | ||||
| 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 | ||||
|   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 | ||||
| @@ -63,8 +69,10 @@ set -e | ||||
| EOF | ||||
| chmod 755 /usr/local/bin/chromium-browser | ||||
|  | ||||
| # start the server  (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff) | ||||
| cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py | ||||
| # 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 | ||||
|  | ||||
| """ | ||||
|  | ||||
| @@ -72,11 +80,23 @@ cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mt | ||||
| 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) | ||||
| @@ -92,7 +112,7 @@ def open_post(txt): | ||||
|     try: | ||||
|         k, v = txt.split(" ", 1) | ||||
|     except: | ||||
|         open_url(txt) | ||||
|         return open_url(txt) | ||||
|  | ||||
|     if k == "key": | ||||
|         sp.call(["xdotool", "key"] + v.split(" ")) | ||||
| @@ -128,6 +148,17 @@ def open_url(txt): | ||||
|     # 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 | ||||
| @@ -136,4 +167,39 @@ def open_url(txt): | ||||
|     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() | ||||
|   | ||||
| @@ -65,6 +65,10 @@ def main(): | ||||
|     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] | ||||
|   | ||||
| @@ -46,13 +46,20 @@ import traceback | ||||
| import http.client  # py2: httplib | ||||
| import urllib.parse | ||||
| import calendar | ||||
| from datetime import datetime | ||||
| from datetime import datetime, timezone | ||||
| from urllib.parse import quote_from_bytes as quote | ||||
| from urllib.parse import unquote_to_bytes as unquote | ||||
|  | ||||
| WINDOWS = sys.platform == "win32" | ||||
| MACOS = platform.system() == "Darwin" | ||||
| info = log = dbg = None | ||||
| UTC = timezone.utc | ||||
|  | ||||
|  | ||||
| def print(*args, **kwargs): | ||||
|     try: | ||||
|         builtins.print(*list(args), **kwargs) | ||||
|     except: | ||||
|         builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs) | ||||
|  | ||||
|  | ||||
| print( | ||||
| @@ -64,6 +71,13 @@ print( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def null_log(msg): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| info = log = dbg = null_log | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from fuse import FUSE, FuseOSError, Operations | ||||
| except: | ||||
| @@ -83,13 +97,6 @@ except: | ||||
|     raise | ||||
|  | ||||
|  | ||||
| def print(*args, **kwargs): | ||||
|     try: | ||||
|         builtins.print(*list(args), **kwargs) | ||||
|     except: | ||||
|         builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs) | ||||
|  | ||||
|  | ||||
| def termsafe(txt): | ||||
|     try: | ||||
|         return txt.encode(sys.stdout.encoding, "backslashreplace").decode( | ||||
| @@ -118,10 +125,6 @@ def fancy_log(msg): | ||||
|     print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="") | ||||
|  | ||||
|  | ||||
| def null_log(msg): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def hexler(binary): | ||||
|     return binary.replace("\r", "\\r").replace("\n", "\\n") | ||||
|     return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary]) | ||||
| @@ -176,7 +179,7 @@ class RecentLog(object): | ||||
|     def put(self, msg): | ||||
|         msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg) | ||||
|         if self.f: | ||||
|             fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)]) | ||||
|             fmsg = " ".join([datetime.now(UTC).strftime("%H%M%S.%f"), str(msg)]) | ||||
|             self.f.write(fmsg.encode("utf-8")) | ||||
|  | ||||
|         with self.mtx: | ||||
|   | ||||
| @@ -20,12 +20,13 @@ import sys | ||||
| import base64 | ||||
| import sqlite3 | ||||
| import argparse | ||||
| from datetime import datetime | ||||
| from datetime import datetime, timezone | ||||
| from urllib.parse import quote_from_bytes as quote | ||||
| from urllib.parse import unquote_to_bytes as unquote | ||||
|  | ||||
|  | ||||
| FS_ENCODING = sys.getfilesystemencoding() | ||||
| UTC = timezone.utc | ||||
|  | ||||
|  | ||||
| class APF(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): | ||||
| @@ -155,11 +156,10 @@ th { | ||||
|                 link = txt.decode("utf-8")[4:] | ||||
|  | ||||
|         sz = "{:,}".format(sz) | ||||
|         dt = datetime.fromtimestamp(at if at > 0 else mt, UTC) | ||||
|         v = [ | ||||
|             w[:16], | ||||
|             datetime.utcfromtimestamp(at if at > 0 else mt).strftime( | ||||
|                 "%Y-%m-%d %H:%M:%S" | ||||
|             ), | ||||
|             dt.strftime("%Y-%m-%d %H:%M:%S"), | ||||
|             sz, | ||||
|             imap.get(ip, ip), | ||||
|         ] | ||||
|   | ||||
| @@ -4,20 +4,21 @@ set -e | ||||
| # runs copyparty (or any other program really) in a chroot | ||||
| # | ||||
| # assumption: these directories, and everything within, are owned by root | ||||
| sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr ) | ||||
|  | ||||
| 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 [...] | ||||
|   ./prisonparty.sh <ROOTDIR> <USER|UID> <GROUP|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 | ||||
|   ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /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 | ||||
|   PYTHONPATH=$PWD ./prisonparty.sh /var/lib/copyparty-jail cpp cpp /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 | ||||
| @@ -27,6 +28,16 @@ exit 1 | ||||
| } | ||||
|  | ||||
|  | ||||
| errs= | ||||
| for c in awk chroot dirname getent lsof mknod mount realpath sed sort stat uniq; do | ||||
| 	command -v $c >/dev/null || { | ||||
| 		echo ERROR: command not found: $c | ||||
| 		errs=1 | ||||
| 	} | ||||
| done | ||||
| [ $errs ] && exit 1 | ||||
|  | ||||
|  | ||||
| # read arguments | ||||
| trap help EXIT | ||||
| jail="$(realpath "$1")"; shift | ||||
| @@ -38,7 +49,7 @@ while true; do | ||||
| 	v="$1"; shift | ||||
| 	[ "$v" = -- ] && break  # end of volumes | ||||
| 	[ "$#" -eq 0 ] && break  # invalid usage | ||||
| 	vols+=( "$(realpath "$v")" ) | ||||
| 	vols+=( "$(realpath "$v" || echo "$v")" ) | ||||
| done | ||||
| pybin="$1"; shift | ||||
| pybin="$(command -v "$pybin")" | ||||
| @@ -57,11 +68,18 @@ cpp="$1"; shift | ||||
| } | ||||
| trap - EXIT | ||||
|  | ||||
| usr="$(getent passwd $uid | cut -d: -f1)" | ||||
| [ "$usr" ] || { echo "ERROR invalid username/uid $uid"; exit 1; } | ||||
| uid="$(getent passwd $uid | cut -d: -f3)" | ||||
|  | ||||
| grp="$(getent group $gid | cut -d: -f1)" | ||||
| [ "$grp" ] || { echo "ERROR invalid groupname/gid $gid"; exit 1; } | ||||
| gid="$(getent group $gid | cut -d: -f3)" | ||||
|  | ||||
| # debug/vis | ||||
| echo | ||||
| echo "chroot-dir = $jail" | ||||
| echo "user:group = $uid:$gid" | ||||
| echo "user:group = $uid:$gid ($usr:$grp)" | ||||
| echo " copyparty = $cpp" | ||||
| echo | ||||
| printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:" | ||||
| @@ -79,32 +97,39 @@ jail="${jail%/}" | ||||
|  | ||||
|  | ||||
| # bind-mount system directories and volumes | ||||
| for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done | ||||
| 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" | ||||
| 		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=$(stat -c%D.%i "$v/"      2>/dev/null || echo a) | ||||
| 	i2=$(stat -c%D.%i "$jail$v/" 2>/dev/null || echo b) | ||||
| 	[ $i1 = $i2 ] && continue | ||||
| 	 | ||||
| 	mount | grep -qF " $jail$v " && echo wtf $i1 $i2 $v && continue | ||||
| 	mkdir -p "$jail$v" | ||||
| 	mount --bind "$v" "$jail$v" | ||||
| done | ||||
| rmdir "$jail/.prisonlock" || true | ||||
|  | ||||
|  | ||||
| cln() { | ||||
| 	rv=$? | ||||
| 	# cleanup if not in use | ||||
| 	lsof "$jail" | grep -qF "$jail" && | ||||
| 		echo "chroot is in use, will not cleanup" || | ||||
| 	trap - EXIT | ||||
| 	wait -f -n $p && rv=0 || rv=$? | ||||
| 	cd / | ||||
| 	echo "stopping chroot..." | ||||
| 	for a in {1..30}; do mkdir "$jail/.prisonlock" && break; sleep 0.1; done | ||||
| 	lsof "$jail" 2>/dev/null | 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 | ||||
| 		LC_ALL=C sort -r | while IFS= read -r v; do | ||||
| 			umount "$v" && echo "umount OK: $v" | ||||
| 		done | ||||
| 	} | ||||
| 	rmdir "$jail/.prisonlock" || true | ||||
| 	exit $rv | ||||
| } | ||||
| trap cln EXIT | ||||
| @@ -115,14 +140,24 @@ 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 HOME="$(getent passwd $uid | cut -d: -f6)" | ||||
| export USER="$usr" | ||||
| export LOGNAME="$USER" | ||||
| #echo "pybin [$pybin]" | ||||
| #echo "pyarg [$pyarg]" | ||||
| #echo "cpp [$cpp]" | ||||
| chroot --userspec=$uid:$gid "$jail" "$pybin" $pyarg "$cpp" "$@" & | ||||
| p=$! | ||||
| trap 'kill $p' INT TERM | ||||
| trap 'kill -USR1 $p' USR1 | ||||
| trap 'trap - INT TERM; kill $p' INT TERM | ||||
| wait | ||||
|   | ||||
| @@ -1,16 +1,20 @@ | ||||
| #!/usr/bin/env python3 | ||||
| from __future__ import print_function, unicode_literals | ||||
| 
 | ||||
| S_VERSION = "1.17" | ||||
| S_BUILD_DT = "2024-05-09" | ||||
| 
 | ||||
| """ | ||||
| up2k.py: upload to copyparty | ||||
| 2023-01-13, v1.2, ed <irc.rizon.net>, MIT-Licensed | ||||
| https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py | ||||
| u2c.py: upload to copyparty | ||||
| 2021, ed <irc.rizon.net>, MIT-Licensed | ||||
| https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py | ||||
| 
 | ||||
| - dependencies: requests | ||||
| - supports python 2.6, 2.7, and 3.3 through 3.12 | ||||
| - if something breaks just try again and it'll autoresume | ||||
| """ | ||||
| 
 | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
| import stat | ||||
| @@ -18,12 +22,15 @@ import math | ||||
| import time | ||||
| import atexit | ||||
| import signal | ||||
| import socket | ||||
| import base64 | ||||
| import hashlib | ||||
| import platform | ||||
| import threading | ||||
| import datetime | ||||
| 
 | ||||
| EXE = bool(getattr(sys, "frozen", False)) | ||||
| 
 | ||||
| try: | ||||
|     import argparse | ||||
| except: | ||||
| @@ -33,8 +40,10 @@ except: | ||||
| 
 | ||||
| try: | ||||
|     import requests | ||||
| except ImportError: | ||||
|     if sys.version_info > (2, 7): | ||||
| except ImportError as ex: | ||||
|     if EXE: | ||||
|         raise | ||||
|     elif sys.version_info > (2, 7): | ||||
|         m = "\nERROR: need 'requests'; please run this command:\n {0} -m pip install --user requests\n" | ||||
|     else: | ||||
|         m = "requests/2.18.4 urllib3/1.23 chardet/3.0.4 certifi/2020.4.5.1 idna/2.7" | ||||
| @@ -42,7 +51,7 @@ except ImportError: | ||||
|         m = "\n  ERROR: need these:\n" + "\n".join(m) + "\n" | ||||
|         m += "\n  for f in *.whl; do unzip $f; done; rm -r *.dist-info\n" | ||||
| 
 | ||||
|     print(m.format(sys.executable)) | ||||
|     print(m.format(sys.executable), "\nspecifically,", ex) | ||||
|     sys.exit(1) | ||||
| 
 | ||||
| 
 | ||||
| @@ -51,6 +60,7 @@ PY2 = sys.version_info < (3,) | ||||
| if PY2: | ||||
|     from Queue import Queue | ||||
|     from urllib import quote, unquote | ||||
|     from urlparse import urlsplit, urlunsplit | ||||
| 
 | ||||
|     sys.dont_write_bytecode = True | ||||
|     bytes = str | ||||
| @@ -58,6 +68,7 @@ else: | ||||
|     from queue import Queue | ||||
|     from urllib.parse import unquote_to_bytes as unquote | ||||
|     from urllib.parse import quote_from_bytes as quote | ||||
|     from urllib.parse import urlsplit, urlunsplit | ||||
| 
 | ||||
|     unicode = str | ||||
| 
 | ||||
| @@ -68,12 +79,21 @@ req_ses = requests.Session() | ||||
| 
 | ||||
| 
 | ||||
| class Daemon(threading.Thread): | ||||
|     def __init__(self, target, name=None, a=None): | ||||
|         # type: (Any, Any, Any) -> None | ||||
|         threading.Thread.__init__(self, target=target, args=a or (), name=name) | ||||
|     def __init__(self, target, name = None, a = None): | ||||
|         threading.Thread.__init__(self, name=name) | ||||
|         self.a = a or () | ||||
|         self.fun = target | ||||
|         self.daemon = True | ||||
|         self.start() | ||||
| 
 | ||||
|     def run(self): | ||||
|         try: | ||||
|             signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM]) | ||||
|         except: | ||||
|             pass | ||||
| 
 | ||||
|         self.fun(*self.a) | ||||
| 
 | ||||
| 
 | ||||
| class File(object): | ||||
|     """an up2k upload task; represents a single file""" | ||||
| @@ -94,12 +114,14 @@ class File(object): | ||||
|         # set by handshake | ||||
|         self.recheck = False  # duplicate; redo handshake after all files done | ||||
|         self.ucids = []  # type: list[str]  # chunks which need to be uploaded | ||||
|         self.wark = None  # type: str | ||||
|         self.url = None  # type: str | ||||
|         self.wark = ""  # type: str | ||||
|         self.url = ""  # type: str | ||||
|         self.nhs = 0 | ||||
| 
 | ||||
|         # set by upload | ||||
|         self.up_b = 0  # type: int | ||||
|         self.up_c = 0  # type: int | ||||
|         self.cd = 0 | ||||
| 
 | ||||
|         # t = "size({}) lmod({}) top({}) rel({}) abs({}) name({})\n" | ||||
|         # eprint(t.format(self.size, self.lmod, self.top, self.rel, self.abs, self.name)) | ||||
| @@ -210,6 +232,7 @@ class MTHash(object): | ||||
| 
 | ||||
|     def hash_at(self, nch): | ||||
|         f = self.f | ||||
|         assert f | ||||
|         ofs = ofs0 = nch * self.csz | ||||
|         hashobj = hashlib.sha512() | ||||
|         chunk_sz = chunk_rem = min(self.csz, self.sz - ofs) | ||||
| @@ -245,7 +268,13 @@ def eprint(*a, **ka): | ||||
| 
 | ||||
| 
 | ||||
| def flushing_print(*a, **ka): | ||||
|     _print(*a, **ka) | ||||
|     try: | ||||
|         _print(*a, **ka) | ||||
|     except: | ||||
|         v = " ".join(str(x) for x in a) | ||||
|         v = v.encode("ascii", "replace").decode("ascii") | ||||
|         _print(v, **ka) | ||||
| 
 | ||||
|     if "flush" not in ka: | ||||
|         sys.stdout.flush() | ||||
| 
 | ||||
| @@ -324,6 +353,32 @@ class CTermsize(object): | ||||
| ss = CTermsize() | ||||
| 
 | ||||
| 
 | ||||
| def undns(url): | ||||
|     usp = urlsplit(url) | ||||
|     hn = usp.hostname | ||||
|     gai = None | ||||
|     eprint("resolving host [{0}] ...".format(hn), end="") | ||||
|     try: | ||||
|         gai = socket.getaddrinfo(hn, None) | ||||
|         hn = gai[0][4][0] | ||||
|     except KeyboardInterrupt: | ||||
|         raise | ||||
|     except: | ||||
|         t = "\n\033[31mfailed to resolve upload destination host;\033[0m\ngai={0}\n" | ||||
|         eprint(t.format(repr(gai))) | ||||
|         raise | ||||
| 
 | ||||
|     if usp.port: | ||||
|         hn = "{0}:{1}".format(hn, usp.port) | ||||
|     if usp.username or usp.password: | ||||
|         hn = "{0}:{1}@{2}".format(usp.username, usp.password, hn) | ||||
| 
 | ||||
|     usp = usp._replace(netloc=hn) | ||||
|     url = urlunsplit(usp) | ||||
|     eprint(" {0}".format(url)) | ||||
|     return url | ||||
| 
 | ||||
| 
 | ||||
| def _scd(err, top): | ||||
|     """non-recursive listing of directory contents, along with stat() info""" | ||||
|     with os.scandir(top) as dh: | ||||
| @@ -369,9 +424,29 @@ def walkdir(err, top, seen): | ||||
|                 err.append((ap, str(ex))) | ||||
| 
 | ||||
| 
 | ||||
| def walkdirs(err, tops): | ||||
| def walkdirs(err, tops, excl): | ||||
|     """recursive statdir for a list of tops, yields [top, relpath, stat]""" | ||||
|     sep = "{0}".format(os.sep).encode("ascii") | ||||
|     if not VT100: | ||||
|         excl = excl.replace("/", r"\\") | ||||
|         za = [] | ||||
|         for td in tops: | ||||
|             try: | ||||
|                 ap = os.path.abspath(os.path.realpath(td)) | ||||
|                 if td[-1:] in (b"\\", b"/"): | ||||
|                     ap += sep | ||||
|             except: | ||||
|                 # maybe cpython #88013 (ok) | ||||
|                 ap = td | ||||
| 
 | ||||
|             za.append(ap) | ||||
| 
 | ||||
|         za = [x if x.startswith(b"\\\\") else b"\\\\?\\" + x for x in za] | ||||
|         za = [x.replace(b"/", b"\\") for x in za] | ||||
|         tops = za | ||||
| 
 | ||||
|     ptn = re.compile(excl.encode("utf-8") or b"\n", re.I) | ||||
| 
 | ||||
|     for top in tops: | ||||
|         isdir = os.path.isdir(top) | ||||
|         if top[-1:] == sep: | ||||
| @@ -384,6 +459,8 @@ def walkdirs(err, tops): | ||||
| 
 | ||||
|         if isdir: | ||||
|             for ap, inf in walkdir(err, top, []): | ||||
|                 if ptn.match(ap): | ||||
|                     continue | ||||
|                 yield stop, ap[len(stop) :].lstrip(sep), inf | ||||
|         else: | ||||
|             d, n = top.rsplit(sep, 1) | ||||
| @@ -396,7 +473,7 @@ def quotep(btxt): | ||||
|     if not PY2: | ||||
|         quot1 = quot1.encode("ascii") | ||||
| 
 | ||||
|     return quot1.replace(b" ", b"+") | ||||
|     return quot1.replace(b" ", b"+")  # type: ignore | ||||
| 
 | ||||
| 
 | ||||
| # from copyparty/util.py | ||||
| @@ -433,7 +510,7 @@ def up2k_chunksize(filesize): | ||||
| 
 | ||||
| # mostly from copyparty/up2k.py | ||||
| def get_hashlist(file, pcb, mth): | ||||
|     # type: (File, any, any) -> None | ||||
|     # type: (File, Any, Any) -> None | ||||
|     """generates the up2k hashlist from file contents, inserts it into `file`""" | ||||
| 
 | ||||
|     chunk_sz = up2k_chunksize(file.size) | ||||
| @@ -492,8 +569,11 @@ def handshake(ar, file, search): | ||||
|     } | ||||
|     if search: | ||||
|         req["srch"] = 1 | ||||
|     elif ar.dr: | ||||
|         req["replace"] = True | ||||
|     else: | ||||
|         if ar.touch: | ||||
|             req["umod"] = True | ||||
|         if ar.ow: | ||||
|             req["replace"] = True | ||||
| 
 | ||||
|     headers = {"Content-Type": "text/plain"}  # <=1.5.1 compat | ||||
|     if pw: | ||||
| @@ -520,7 +600,11 @@ def handshake(ar, file, search): | ||||
|         except Exception as ex: | ||||
|             em = str(ex).split("SSLError(")[-1].split("\nURL: ")[0].strip() | ||||
| 
 | ||||
|             if sc == 422 or "<pre>partial upload exists at a different" in txt: | ||||
|             if ( | ||||
|                 sc == 422 | ||||
|                 or "<pre>partial upload exists at a different" in txt | ||||
|                 or "<pre>source file busy; please try again" in txt | ||||
|             ): | ||||
|                 file.recheck = True | ||||
|                 return [], False | ||||
|             elif sc == 409 or "<pre>upload rejected, file already exists" in txt: | ||||
| @@ -529,7 +613,7 @@ def handshake(ar, file, search): | ||||
|                 raise | ||||
| 
 | ||||
|             eprint("handshake failed, retrying: {0}\n  {1}\n\n".format(file.name, em)) | ||||
|             time.sleep(1) | ||||
|             time.sleep(ar.cd) | ||||
| 
 | ||||
|     try: | ||||
|         r = r.json() | ||||
| @@ -552,8 +636,8 @@ def handshake(ar, file, search): | ||||
|     return r["hash"], r["sprs"] | ||||
| 
 | ||||
| 
 | ||||
| def upload(file, cid, pw): | ||||
|     # type: (File, str, str) -> None | ||||
| def upload(file, cid, pw, stats): | ||||
|     # type: (File, str, str, str) -> None | ||||
|     """upload one specific chunk, `cid` (a chunk-hash)""" | ||||
| 
 | ||||
|     headers = { | ||||
| @@ -561,6 +645,10 @@ def upload(file, cid, pw): | ||||
|         "X-Up2k-Wark": file.wark, | ||||
|         "Content-Type": "application/octet-stream", | ||||
|     } | ||||
| 
 | ||||
|     if stats: | ||||
|         headers["X-Up2k-Stat"] = stats | ||||
| 
 | ||||
|     if pw: | ||||
|         headers["Cookie"] = "=".join(["cppwd", pw]) | ||||
| 
 | ||||
| @@ -587,7 +675,7 @@ class Ctl(object): | ||||
|         nfiles = 0 | ||||
|         nbytes = 0 | ||||
|         err = [] | ||||
|         for _, _, inf in walkdirs(err, ar.files): | ||||
|         for _, _, inf in walkdirs(err, ar.files, ar.x): | ||||
|             if stat.S_ISDIR(inf.st_mode): | ||||
|                 continue | ||||
| 
 | ||||
| @@ -615,6 +703,8 @@ class Ctl(object): | ||||
|         return nfiles, nbytes | ||||
| 
 | ||||
|     def __init__(self, ar, stats=None): | ||||
|         self.ok = False | ||||
|         self.errs = 0 | ||||
|         self.ar = ar | ||||
|         self.stats = stats or self._scan() | ||||
|         if not self.stats: | ||||
| @@ -628,7 +718,9 @@ class Ctl(object): | ||||
|         if ar.te: | ||||
|             req_ses.verify = ar.te | ||||
| 
 | ||||
|         self.filegen = walkdirs([], ar.files) | ||||
|         self.filegen = walkdirs([], ar.files, ar.x) | ||||
|         self.recheck = []  # type: list[File] | ||||
| 
 | ||||
|         if ar.safe: | ||||
|             self._safe() | ||||
|         else: | ||||
| @@ -647,11 +739,11 @@ class Ctl(object): | ||||
|             self.t0 = time.time() | ||||
|             self.t0_up = None | ||||
|             self.spd = None | ||||
|             self.eta = "99:99:99" | ||||
| 
 | ||||
|             self.mutex = threading.Lock() | ||||
|             self.q_handshake = Queue()  # type: Queue[File] | ||||
|             self.q_upload = Queue()  # type: Queue[tuple[File, str]] | ||||
|             self.recheck = []  # type: list[File] | ||||
| 
 | ||||
|             self.st_hash = [None, "(idle, starting...)"]  # type: tuple[File, int] | ||||
|             self.st_up = [None, "(idle, starting...)"]  # type: tuple[File, int] | ||||
| @@ -660,6 +752,8 @@ class Ctl(object): | ||||
| 
 | ||||
|             self._fancy() | ||||
| 
 | ||||
|         self.ok = not self.errs | ||||
| 
 | ||||
|     def _safe(self): | ||||
|         """minimal basic slow boring fallback codepath""" | ||||
|         search = self.ar.s | ||||
| @@ -693,7 +787,8 @@ class Ctl(object): | ||||
|                 ncs = len(hs) | ||||
|                 for nc, cid in enumerate(hs): | ||||
|                     print("  {0} up {1}".format(ncs - nc, cid)) | ||||
|                     upload(file, cid, self.ar.a) | ||||
|                     stats = "{0}/0/0/{1}".format(nf, self.nfiles - nf) | ||||
|                     upload(file, cid, self.ar.a, stats) | ||||
| 
 | ||||
|             print("  ok!") | ||||
|             if file.recheck: | ||||
| @@ -760,20 +855,20 @@ class Ctl(object): | ||||
|                 txt = " " | ||||
| 
 | ||||
|             if not self.up_br: | ||||
|                 spd = self.hash_b / (time.time() - self.t0) | ||||
|                 eta = (self.nbytes - self.hash_b) / (spd + 1) | ||||
|                 spd = self.hash_b / ((time.time() - self.t0) or 1) | ||||
|                 eta = (self.nbytes - self.hash_b) / (spd or 1) | ||||
|             else: | ||||
|                 spd = self.up_br / (time.time() - self.t0_up) | ||||
|                 spd = self.up_br / ((time.time() - self.t0_up) or 1) | ||||
|                 spd = self.spd = (self.spd or spd) * 0.9 + spd * 0.1 | ||||
|                 eta = (self.nbytes - self.up_b) / (spd + 1) | ||||
|                 eta = (self.nbytes - self.up_b) / (spd or 1) | ||||
| 
 | ||||
|             spd = humansize(spd) | ||||
|             eta = str(datetime.timedelta(seconds=int(eta))) | ||||
|             self.eta = str(datetime.timedelta(seconds=int(eta))) | ||||
|             sleft = humansize(self.nbytes - self.up_b) | ||||
|             nleft = self.nfiles - self.up_f | ||||
|             tail = "\033[K\033[u" if VT100 and not self.ar.ns else "\r" | ||||
| 
 | ||||
|             t = "{0} eta @ {1}/s, {2}, {3}# left".format(eta, spd, sleft, nleft) | ||||
|             t = "{0} eta @ {1}/s, {2}, {3}# left".format(self.eta, spd, sleft, nleft) | ||||
|             eprint(txt + "\033]0;{0}\033\\\r{0}{1}".format(t, tail)) | ||||
| 
 | ||||
|         if not self.recheck: | ||||
| @@ -791,6 +886,8 @@ class Ctl(object): | ||||
|         self.st_hash = [file, ofs] | ||||
| 
 | ||||
|     def hasher(self): | ||||
|         ptn = re.compile(self.ar.x.encode("utf-8"), re.I) if self.ar.x else None | ||||
|         sep = "{0}".format(os.sep).encode("ascii") | ||||
|         prd = None | ||||
|         ls = {} | ||||
|         for top, rel, inf in self.filegen: | ||||
| @@ -809,9 +906,9 @@ class Ctl(object): | ||||
|                         print("      ls ~{0}".format(srd)) | ||||
|                         zb = self.ar.url.encode("utf-8") | ||||
|                         zb += quotep(rd.replace(b"\\", b"/")) | ||||
|                         r = req_ses.get(zb + b"?ls&dots", headers=headers) | ||||
|                         r = req_ses.get(zb + b"?ls<&dots", headers=headers) | ||||
|                         if not r: | ||||
|                             raise Exception("HTTP {}".format(r.status_code)) | ||||
|                             raise Exception("HTTP {0}".format(r.status_code)) | ||||
| 
 | ||||
|                         j = r.json() | ||||
|                         for f in j["dirs"] + j["files"]: | ||||
| @@ -823,13 +920,29 @@ class Ctl(object): | ||||
|                     if self.ar.drd: | ||||
|                         dp = os.path.join(top, rd) | ||||
|                         lnodes = set(os.listdir(dp)) | ||||
|                         bnames = [x for x in ls if x not in lnodes] | ||||
|                         if bnames: | ||||
|                             vpath = self.ar.url.split("://")[-1].split("/", 1)[-1] | ||||
|                             names = [x.decode("utf-8", "replace") for x in bnames] | ||||
|                             locs = [vpath + srd + "/" + x for x in names] | ||||
|                             print("DELETING ~{0}/#{1}".format(srd, len(names))) | ||||
|                             req_ses.post(self.ar.url + "?delete", json=locs) | ||||
|                         if ptn: | ||||
|                             zs = dp.replace(sep, b"/").rstrip(b"/") + b"/" | ||||
|                             zls = [zs + x for x in lnodes] | ||||
|                             zls = [x for x in zls if not ptn.match(x)] | ||||
|                             lnodes = [x.split(b"/")[-1] for x in zls] | ||||
|                         bnames = [x for x in ls if x not in lnodes and x != b".hist"] | ||||
|                         vpath = self.ar.url.split("://")[-1].split("/", 1)[-1] | ||||
|                         names = [x.decode("utf-8", "replace") for x in bnames] | ||||
|                         locs = [vpath + srd + "/" + x for x in names] | ||||
|                         while locs: | ||||
|                             req = locs | ||||
|                             while req: | ||||
|                                 print("DELETING ~%s/#%s" % (srd, len(req))) | ||||
|                                 r = req_ses.post(self.ar.url + "?delete", json=req) | ||||
|                                 if r.status_code == 413 and "json 2big" in r.text: | ||||
|                                     print(" (delete request too big; slicing...)") | ||||
|                                     req = req[: len(req) // 2] | ||||
|                                     continue | ||||
|                                 elif not r: | ||||
|                                     t = "delete request failed: %r %s" | ||||
|                                     raise Exception(t % (r, r.text)) | ||||
|                                 break | ||||
|                             locs = locs[len(req) :] | ||||
| 
 | ||||
|             if isdir: | ||||
|                 continue | ||||
| @@ -882,10 +995,22 @@ class Ctl(object): | ||||
|                 self.q_upload.put(None) | ||||
|                 break | ||||
| 
 | ||||
|             upath = file.abs.decode("utf-8", "replace") | ||||
|             if not VT100: | ||||
|                 upath = upath.lstrip("\\?") | ||||
| 
 | ||||
|             file.nhs += 1 | ||||
|             if file.nhs > 32: | ||||
|                 print("ERROR: giving up on file %s" % (upath)) | ||||
|                 self.errs += 1 | ||||
|                 continue | ||||
| 
 | ||||
|             with self.mutex: | ||||
|                 self.handshaker_busy += 1 | ||||
| 
 | ||||
|             upath = file.abs.decode("utf-8", "replace") | ||||
|             while time.time() < file.cd: | ||||
|                 time.sleep(0.1) | ||||
| 
 | ||||
|             hs, sprs = handshake(self.ar, file, search) | ||||
|             if search: | ||||
|                 if hs: | ||||
| @@ -951,11 +1076,23 @@ class Ctl(object): | ||||
|                 self.uploader_busy += 1 | ||||
|                 self.t0_up = self.t0_up or time.time() | ||||
| 
 | ||||
|             stats = "%d/%d/%d/%d %d/%d %s" % ( | ||||
|                 self.up_f, | ||||
|                 len(self.recheck), | ||||
|                 self.uploader_busy, | ||||
|                 self.nfiles - self.up_f, | ||||
|                 self.nbytes // (1024 * 1024), | ||||
|                 (self.nbytes - self.up_b) // (1024 * 1024), | ||||
|                 self.eta, | ||||
|             ) | ||||
| 
 | ||||
|             file, cid = task | ||||
|             try: | ||||
|                 upload(file, cid, self.ar.a) | ||||
|             except: | ||||
|                 eprint("upload failed, retrying: {0} #{1}\n".format(file.name, cid[:8])) | ||||
|                 upload(file, cid, self.ar.a, stats) | ||||
|             except Exception as ex: | ||||
|                 t = "upload failed, retrying: {0} #{1} ({2})\n" | ||||
|                 eprint(t.format(file.name, cid[:8], ex)) | ||||
|                 file.cd = time.time() + self.ar.cd | ||||
|                 # handshake will fix it | ||||
| 
 | ||||
|             with self.mutex: | ||||
| @@ -989,8 +1126,15 @@ def main(): | ||||
|     cores = (os.cpu_count() if hasattr(os, "cpu_count") else 0) or 2 | ||||
|     hcores = min(cores, 3)  # 4% faster than 4+ on py3.9 @ r5-4500U | ||||
| 
 | ||||
|     ver = "{0}, v{1}".format(S_BUILD_DT, S_VERSION) | ||||
|     if "--version" in sys.argv: | ||||
|         print(ver) | ||||
|         return | ||||
| 
 | ||||
|     sys.argv = [x for x in sys.argv if x != "--ws"] | ||||
| 
 | ||||
|     # fmt: off | ||||
|     ap = app = argparse.ArgumentParser(formatter_class=APF, epilog=""" | ||||
|     ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool, " + ver, epilog=""" | ||||
| NOTE: | ||||
| source file/folder selection uses rsync syntax, meaning that: | ||||
|   "foo" uploads the entire folder to URL/foo/ | ||||
| @@ -1002,22 +1146,27 @@ source file/folder selection uses rsync syntax, meaning that: | ||||
|     ap.add_argument("-v", action="store_true", help="verbose") | ||||
|     ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath") | ||||
|     ap.add_argument("-s", action="store_true", help="file-search (disables upload)") | ||||
|     ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'") | ||||
|     ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible") | ||||
|     ap.add_argument("--touch", action="store_true", help="if last-modified timestamps differ, push local to server (need write+delete perms)") | ||||
|     ap.add_argument("--ow", action="store_true", help="overwrite existing files instead of autorenaming") | ||||
|     ap.add_argument("--version", action="store_true", help="show version and exit") | ||||
| 
 | ||||
|     ap = app.add_argument_group("compatibility") | ||||
|     ap.add_argument("--cls", action="store_true", help="clear screen before start") | ||||
|     ap.add_argument("--ws", action="store_true", help="copyparty is running on windows; wait before deleting files after uploading") | ||||
|     ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)") | ||||
| 
 | ||||
|     ap = app.add_argument_group("folder sync") | ||||
|     ap.add_argument("--dl", action="store_true", help="delete local files after uploading") | ||||
|     ap.add_argument("--dr", action="store_true", help="delete remote files which don't exist locally") | ||||
|     ap.add_argument("--dr", action="store_true", help="delete remote files which don't exist locally (implies --ow)") | ||||
|     ap.add_argument("--drd", action="store_true", help="delete remote files during upload instead of afterwards; reduces peak disk space usage, but will reupload instead of detecting renames") | ||||
| 
 | ||||
|     ap = app.add_argument_group("performance tweaks") | ||||
|     ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections") | ||||
|     ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing") | ||||
|     ap.add_argument("-nh", action="store_true", help="disable hashing while uploading") | ||||
|     ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles)") | ||||
|     ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)") | ||||
|     ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload") | ||||
|     ap.add_argument("--safe", action="store_true", help="use simple fallback approach") | ||||
|     ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)") | ||||
| 
 | ||||
| @@ -1026,10 +1175,22 @@ source file/folder selection uses rsync syntax, meaning that: | ||||
|     ap.add_argument("-td", action="store_true", help="disable certificate check") | ||||
|     # fmt: on | ||||
| 
 | ||||
|     ar = app.parse_args() | ||||
|     try: | ||||
|         ar = app.parse_args() | ||||
|     finally: | ||||
|         if EXE and not sys.argv[1:]: | ||||
|             eprint("*** hit enter to exit ***") | ||||
|             try: | ||||
|                 input() | ||||
|             except: | ||||
|                 pass | ||||
| 
 | ||||
|     if ar.drd: | ||||
|         ar.dr = True | ||||
| 
 | ||||
|     if ar.dr: | ||||
|         ar.ow = True | ||||
| 
 | ||||
|     for k in "dl dr drd".split(): | ||||
|         errs = [] | ||||
|         if ar.safe and getattr(ar, k): | ||||
| @@ -1040,7 +1201,7 @@ source file/folder selection uses rsync syntax, meaning that: | ||||
| 
 | ||||
|     ar.files = [ | ||||
|         os.path.abspath(os.path.realpath(x.encode("utf-8"))) | ||||
|         + (x[-1:] if x[-1:] == os.sep else "").encode("utf-8") | ||||
|         + (x[-1:] if x[-1:] in ("\\", "/") else "").encode("utf-8") | ||||
|         for x in ar.files | ||||
|     ] | ||||
| 
 | ||||
| @@ -1050,24 +1211,35 @@ source file/folder selection uses rsync syntax, meaning that: | ||||
| 
 | ||||
|     if ar.a and ar.a.startswith("$"): | ||||
|         fn = ar.a[1:] | ||||
|         print("reading password from file [{}]".format(fn)) | ||||
|         print("reading password from file [{0}]".format(fn)) | ||||
|         with open(fn, "rb") as f: | ||||
|             ar.a = f.read().decode("utf-8").strip() | ||||
| 
 | ||||
|     for n in range(ar.rh): | ||||
|         try: | ||||
|             ar.url = undns(ar.url) | ||||
|             break | ||||
|         except KeyboardInterrupt: | ||||
|             raise | ||||
|         except: | ||||
|             if n > ar.rh - 2: | ||||
|                 raise | ||||
| 
 | ||||
|     if ar.cls: | ||||
|         print("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="") | ||||
|         eprint("\033[H\033[2J\033[3J", end="") | ||||
| 
 | ||||
|     ctl = Ctl(ar) | ||||
| 
 | ||||
|     if ar.dr and not ar.drd: | ||||
|     if ar.dr and not ar.drd and ctl.ok: | ||||
|         print("\npass 2/2: delete") | ||||
|         if getattr(ctl, "up_br") and ar.ws: | ||||
|             # wait for up2k to mtime if there was uploads | ||||
|             time.sleep(4) | ||||
| 
 | ||||
|         ar.drd = True | ||||
|         ar.z = True | ||||
|         Ctl(ar, ctl.stats) | ||||
|         ctl = Ctl(ar, ctl.stats) | ||||
| 
 | ||||
|     if ctl.errs: | ||||
|         print("WARNING: %d errors" % (ctl.errs)) | ||||
| 
 | ||||
|     sys.exit(0 if ctl.ok else 1) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
| @@ -66,7 +66,7 @@ def main(): | ||||
|                 ofs = ln.find("{") | ||||
|                 j = json.loads(ln[ofs:]) | ||||
|             except: | ||||
|                 pass | ||||
|                 continue | ||||
|  | ||||
|             w = j["wark"] | ||||
|             if db.execute("select w from up where w = ?", (w,)).fetchone(): | ||||
|   | ||||
| @@ -16,11 +16,13 @@ | ||||
| * sharex config file to upload screenshots and grab the URL | ||||
| * `RequestURL`: full URL to the target folder | ||||
| * `pw`: password (remove the `pw` line if anon-write) | ||||
| * the `act:bput` thing is optional since copyparty v1.9.29 | ||||
| * using an older sharex version, maybe sharex v12.1.1 for example? dw fam i got your back 👉😎👉 [`sharex12.sxcu`](sharex12.sxcu) | ||||
|  | ||||
| however if your copyparty is behind a reverse-proxy, you may want to use [`sharex-html.sxcu`](sharex-html.sxcu) instead: | ||||
| * `RequestURL`: full URL to the target folder | ||||
| * `URL`: full URL to the root folder (with trailing slash) followed by `$regex:1|1$` | ||||
| * `pw`: password (remove `Parameters` if anon-write) | ||||
| ### [`send-to-cpp.contextlet.json`](send-to-cpp.contextlet.json) | ||||
| * browser integration, kind of? custom rightclick actions and stuff | ||||
| * rightclick a pic and send it to copyparty straight from your browser | ||||
| * for the [contextlet](https://addons.mozilla.org/en-US/firefox/addon/contextlets/) firefox extension | ||||
|  | ||||
| ### [`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 | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| # when running copyparty behind a reverse proxy, | ||||
| # the following arguments are recommended: | ||||
| # | ||||
| #   --http-only     lower latency on initial connection | ||||
| #   -i 127.0.0.1    only accept connections from nginx | ||||
| # | ||||
| # if you are doing location-based proxying (such as `/stuff` below) | ||||
|   | ||||
| @@ -1,14 +1,44 @@ | ||||
| #!/bin/bash | ||||
| set -e | ||||
|  | ||||
| cat >/dev/null <<'EOF' | ||||
|  | ||||
| NOTE: copyparty is now able to do this automatically; | ||||
| however you may wish to use this script instead if | ||||
| 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" ] && { | ||||
| 	echo "need arg 1: ca name" | ||||
| 	echo "need arg 2: server fqdn and/or IPs, comma-separated" | ||||
| 	echo "optional arg 3: if set, write cert into copyparty cfg" | ||||
| [ -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 | ||||
| } | ||||
|  | ||||
| @@ -59,12 +89,14 @@ show() { | ||||
| } | ||||
| show ca.pem | ||||
| show "$srv_fqdn.pem" | ||||
|  | ||||
| echo | ||||
| echo "successfully generated new certificates" | ||||
|  | ||||
| # write cert into copyparty config | ||||
| [ -z "$3" ] || { | ||||
| 	mkdir -p ~/.config/copyparty | ||||
| 	cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem  | ||||
| 	echo "successfully replaced copyparty certificate" | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| <head> | ||||
| 	<meta charset="utf-8"> | ||||
| 	<title>⇆🎉 redirect</title> | ||||
| 	<title>💾🎉 redirect</title> | ||||
| 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
| 	<style> | ||||
|  | ||||
| @@ -26,8 +26,8 @@ a { | ||||
| 	<script> | ||||
|  | ||||
| var a = document.getElementById('redir'), | ||||
| 	proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http', | ||||
| 	loc = window.location.hostname || '127.0.0.1', | ||||
| 	proto = location.protocol.indexOf('https') === 0 ? 'https' : 'http', | ||||
| 	loc = location.hostname || '127.0.0.1', | ||||
| 	port = a.getAttribute('href').split(':').pop().split('/')[0], | ||||
| 	url = proto + '://' + loc + ':' + port + '/'; | ||||
|  | ||||
| @@ -35,7 +35,7 @@ a.setAttribute('href', url); | ||||
| document.getElementById('desc').innerHTML = 'redirecting to'; | ||||
|  | ||||
| setTimeout(function() { | ||||
| 	window.location.href = url; | ||||
| 	location.href = url; | ||||
| }, 500); | ||||
|  | ||||
| </script> | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								contrib/ios/upload-to-copyparty.shortcut
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								contrib/ios/upload-to-copyparty.shortcut
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,7 +1,6 @@ | ||||
| # when running copyparty behind a reverse proxy, | ||||
| # the following arguments are recommended: | ||||
| # | ||||
| #   --http-only     lower latency on initial connection | ||||
| #   -i 127.0.0.1    only accept connections from nginx | ||||
| # | ||||
| # -nc must match or exceed the webserver's max number of concurrent clients; | ||||
| @@ -9,12 +8,20 @@ | ||||
| # nginx default is 512  (worker_processes 1, worker_connections 512) | ||||
| # | ||||
| # 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 | ||||
| # | ||||
| # if you are behind cloudflare (or another protection service), | ||||
| # remember to reject all connections which are not coming from your | ||||
| # protection service -- for cloudflare in particular, you can | ||||
| # generate the list of permitted IP ranges like so: | ||||
| #   (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf | ||||
| # | ||||
| # and then enable it below by uncomenting the cloudflare-only.conf line | ||||
|  | ||||
| upstream cpp { | ||||
| 	server 127.0.0.1:3923; | ||||
| 	server 127.0.0.1:3923 fail_timeout=1s; | ||||
| 	keepalive 1; | ||||
| } | ||||
| server { | ||||
| @@ -22,7 +29,10 @@ server { | ||||
| 	listen [::]:443 ssl; | ||||
|  | ||||
| 	server_name fs.example.com; | ||||
| 	 | ||||
|  | ||||
| 	# uncomment the following line to reject non-cloudflare connections, ensuring client IPs cannot be spoofed: | ||||
| 	#include /etc/nginx/cloudflare-only.conf; | ||||
|  | ||||
| 	location / { | ||||
| 		proxy_pass http://cpp; | ||||
| 		proxy_redirect off; | ||||
| @@ -35,7 +45,15 @@ server { | ||||
| 		proxy_set_header   Host              $host; | ||||
| 		proxy_set_header   X-Real-IP         $remote_addr; | ||||
| 		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   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; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										56
									
								
								contrib/package/arch/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								contrib/package/arch/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| # Maintainer: icxes <dev.null@need.moe> | ||||
| pkgname=copyparty | ||||
| pkgver="1.13.1" | ||||
| pkgrel=1 | ||||
| pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++" | ||||
| 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" | ||||
|             "cfssl: generate TLS certificates on startup (pointless when reverse-proxied)" | ||||
|             "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=("f103b784c423a45fbab47c584e4cc53d887fe0616f803bffe009fbfdab3963d7") | ||||
|  | ||||
| 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/` | ||||
							
								
								
									
										33
									
								
								contrib/package/arch/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								contrib/package/arch/prisonparty.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| # this will start `/usr/bin/copyparty-sfx.py` | ||||
| # in a chroot, preventing accidental access elsewhere, | ||||
| # and read copyparty config from `/etc/copyparty.d/*.conf` | ||||
| # | ||||
| # expose additional filesystem locations to copyparty | ||||
| #   by listing them between the last `cpp` and `--` | ||||
| # | ||||
| # `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) | ||||
| # | ||||
| # 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 cpp cpp \ | ||||
|   /etc/copyparty.d \ | ||||
|   -- \ | ||||
|   /usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
							
								
								
									
										63
									
								
								contrib/package/nix/copyparty/default.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								contrib/package/nix/copyparty/default.nix
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| { 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, | ||||
|  | ||||
| # generate TLS certificates on startup (pointless when reverse-proxied) | ||||
| withCertgen ? false, | ||||
|  | ||||
| # 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 withCertgen cfssl | ||||
|     ++ 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.13.1/copyparty-sfx.py", | ||||
|     "version": "1.13.1", | ||||
|     "hash": "sha256-NFfnveCrR1SbiNlibVyU3UPePLUGJMc4XZvWdksXNd8=" | ||||
| } | ||||
							
								
								
									
										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() | ||||
| @@ -1,4 +1,9 @@ | ||||
| <!-- | ||||
|   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 | ||||
|  | ||||
| @@ -11,7 +16,7 @@ | ||||
|  | ||||
|     /* make the up2k ui REALLY minimal by hiding a bunch of stuff: */ | ||||
|  | ||||
|     #ops, #tree, #path, #epi+h2,  /* main tabs and navigators (tree/breadcrumbs) */ | ||||
|     #ops, #tree, #path, #wfp,  /* main tabs and navigators (tree/breadcrumbs) */ | ||||
|  | ||||
|     #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw),  /* most of the config options */ | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ almost the same as minimal-up2k.html except this one...: | ||||
| var u2min = ` | ||||
| <style> | ||||
|  | ||||
| #ops, #path, #tree, #files, #epi+div+h2, | ||||
| #ops, #path, #tree, #files, #wfp, | ||||
| #u2conf td.c+.c, #u2cards, #srch_dz, #srch_zd { | ||||
|   display: none !important; | ||||
| } | ||||
| @@ -55,5 +55,5 @@ var u2min = ` | ||||
| if (!has(perms, 'read')) { | ||||
|   var e2 = mknod('div'); | ||||
|   e2.innerHTML = u2min; | ||||
|   ebi('wrap').insertBefore(e2, QS('#epi+h2')); | ||||
|   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 | ||||
|     }); | ||||
|  | ||||
| })(); | ||||
| @@ -10,7 +10,7 @@ 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}" | ||||
| copyparty_command="/usr/local/bin/python3.9 /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}" | ||||
|   | ||||
							
								
								
									
										11
									
								
								contrib/send-to-cpp.contextlet.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								contrib/send-to-cpp.contextlet.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|     "code": "// https://addons.mozilla.org/en-US/firefox/addon/contextlets/\n// https://github.com/davidmhammond/contextlets\n\nvar url = 'http://partybox.local:3923/';\nvar pw = 'wark';\n\nvar xhr = new XMLHttpRequest();\nxhr.msg = this.info.linkUrl || this.info.srcUrl;\nxhr.open('POST', url, true);\nxhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');\nxhr.setRequestHeader('PW', pw);\nxhr.send('msg=' + xhr.msg);\n", | ||||
|     "contexts": [ | ||||
|         "link" | ||||
|     ], | ||||
|     "icons": null, | ||||
|     "patterns": "", | ||||
|     "scope": "background", | ||||
|     "title": "send to cpp", | ||||
|     "type": "normal" | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| { | ||||
|   "Version": "13.5.0", | ||||
|   "Name": "copyparty-html", | ||||
|   "DestinationType": "ImageUploader", | ||||
|   "RequestMethod": "POST", | ||||
|   "RequestURL": "http://127.0.0.1:3923/sharex", | ||||
|   "Parameters": { | ||||
|     "pw": "wark" | ||||
|   }, | ||||
|   "Body": "MultipartFormData", | ||||
|   "Arguments": { | ||||
|     "act": "bput" | ||||
|   }, | ||||
|   "FileFormName": "f", | ||||
|   "RegexList": [ | ||||
|     "bytes // <a href=\"/([^\"]+)\"" | ||||
|   ], | ||||
|   "URL": "http://127.0.0.1:3923/$regex:1|1$" | ||||
| } | ||||
| @@ -1,17 +1,19 @@ | ||||
| { | ||||
|   "Version": "13.5.0", | ||||
|   "Version": "15.0.0", | ||||
|   "Name": "copyparty", | ||||
|   "DestinationType": "ImageUploader", | ||||
|   "RequestMethod": "POST", | ||||
|   "RequestURL": "http://127.0.0.1:3923/sharex", | ||||
|   "Parameters": { | ||||
|     "pw": "wark", | ||||
|     "j": null | ||||
|   }, | ||||
|   "Headers": { | ||||
|     "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE" | ||||
|   }, | ||||
|   "Body": "MultipartFormData", | ||||
|   "Arguments": { | ||||
|     "act": "bput" | ||||
|   }, | ||||
|   "FileFormName": "f", | ||||
|   "URL": "$json:files[0].url$" | ||||
|   "URL": "{json:files[0].url}" | ||||
| } | ||||
|   | ||||
							
								
								
									
										13
									
								
								contrib/sharex12.sxcu
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								contrib/sharex12.sxcu
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|   "Name": "copyparty", | ||||
|   "DestinationType": "ImageUploader, TextUploader, FileUploader", | ||||
|   "RequestURL": "http://127.0.0.1:3923/sharex", | ||||
|   "FileFormName": "f", | ||||
|   "Arguments": { | ||||
|     "act": "bput" | ||||
|   }, | ||||
|   "Headers": { | ||||
|     "accept": "url", | ||||
|     "pw": "PUT_YOUR_PASSWORD_HERE_MY_DUDE" | ||||
|   } | ||||
| } | ||||
| @@ -1,3 +1,6 @@ | ||||
| # 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 | ||||
|   | ||||
							
								
								
									
										42
									
								
								contrib/systemd/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								contrib/systemd/copyparty.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| # not actually YAML but lets pretend: | ||||
| # -*- mode: yaml -*- | ||||
| # vim: ft=yaml: | ||||
|  | ||||
|  | ||||
| # put this file in /etc/ | ||||
|  | ||||
|  | ||||
| [global] | ||||
|   e2dsa  # enable file indexing and filesystem scanning | ||||
|   e2ts   # and enable multimedia indexing | ||||
|   ansi   # and colors in log messages | ||||
|  | ||||
|   # disable logging to stdout/journalctl and log to a file instead; | ||||
|   # $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd) | ||||
|   # and copyparty replaces %Y-%m%d with Year-MonthDay, so the | ||||
|   # full path will be something like /var/log/copyparty/2023-1130.txt | ||||
|   # (note: enable compression by adding .xz at the end) | ||||
|   q, lo: $LOGS_DIRECTORY/%Y-%m%d.log | ||||
|  | ||||
|   # p: 80,443,3923   # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE) | ||||
|   # i: 127.0.0.1     # only allow connections from localhost (reverse-proxies) | ||||
|   # ftp: 3921        # enable ftp server on port 3921 | ||||
|   # p: 3939          # listen on another port | ||||
|   # df: 16           # stop accepting uploads if less than 16 GB free disk space | ||||
|   # ver              # show copyparty version in the controlpanel | ||||
|   # grid             # show thumbnails/grid-view by default | ||||
|   # theme: 2         # monokai | ||||
|   # name: datasaver  # change the server-name that's displayed in the browser | ||||
|   # stats, nos-dup   # enable the prometheus endpoint, but disable the dupes counter (too slow) | ||||
|   # no-robots, force-js  # make it harder for search engines to read your server | ||||
|  | ||||
|  | ||||
| [accounts] | ||||
|   ed: wark  # username: password | ||||
|  | ||||
|  | ||||
| [/]            # create a volume at "/" (the webroot), which will | ||||
|   /mnt         # share the contents of the "/mnt" folder | ||||
|   accs: | ||||
|     rw: *      # everyone gets read-write access, but | ||||
|     rwmda: ed  # the user "ed" gets read-write-move-delete-admin | ||||
| @@ -1,23 +1,27 @@ | ||||
| # this will start `/usr/local/bin/copyparty-sfx.py` | ||||
| # and share '/mnt' with anonymous read+write | ||||
| # this will start `/usr/local/bin/copyparty-sfx.py` and | ||||
| # read copyparty config from `/etc/copyparty.conf`, for example: | ||||
| # https://github.com/9001/copyparty/blob/hovudstraum/contrib/systemd/copyparty.conf | ||||
| # | ||||
| # installation: | ||||
| #   cp -pv copyparty.service /etc/systemd/system | ||||
| #   restorecon -vr /etc/systemd/system/copyparty.service | ||||
| #   firewall-cmd --permanent --add-port={80,443,3923}/tcp  # --zone=libvirt | ||||
| #   wget https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py -O /usr/local/bin/copyparty-sfx.py | ||||
| #   useradd -r -s /sbin/nologin -d /var/lib/copyparty copyparty | ||||
| #   firewall-cmd --permanent --add-port=3923/tcp  # --zone=libvirt | ||||
| #   firewall-cmd --reload | ||||
| #   cp -pv copyparty.service /etc/systemd/system/ | ||||
| #   cp -pv copyparty.conf /etc/ | ||||
| #   restorecon -vr /etc/systemd/system/copyparty.service  # on fedora/rhel | ||||
| #   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 | ||||
| #   tail -Fn 100 /var/log/copyparty/$(date +%Y-%m%d.log) | ||||
| # | ||||
| # you may want to: | ||||
| #   change "User=cpp" and "/home/cpp/" to another user | ||||
| #   remove the nft lines to only listen on port 3923 | ||||
| #  - change "User=copyparty" and "/var/lib/copyparty/" to another user | ||||
| #  - edit /etc/copyparty.conf to configure copyparty | ||||
| # 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 | ||||
| #  - change '/usr/bin/python3' to another interpreter | ||||
| # | ||||
| # with `Type=notify`, copyparty will signal systemd when it is ready to | ||||
| #   accept connections; correctly delaying units depending on copyparty. | ||||
| @@ -25,11 +29,9 @@ | ||||
| #   python disabling line-buffering, so messages are out-of-order: | ||||
| #   https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png | ||||
| # | ||||
| # unless you add -q to disable logging, you may want to remove the | ||||
| #   following line to allow buffering (slightly better performance): | ||||
| #   Environment=PYTHONUNBUFFERED=x | ||||
| # | ||||
| # keep ExecStartPre before ExecStart, at least on rhel8 | ||||
| ######################################################################## | ||||
| ######################################################################## | ||||
|  | ||||
|  | ||||
| [Unit] | ||||
| Description=copyparty file server | ||||
| @@ -39,23 +41,52 @@ Type=notify | ||||
| SyslogIdentifier=copyparty | ||||
| Environment=PYTHONUNBUFFERED=x | ||||
| ExecReload=/bin/kill -s USR1 $MAINPID | ||||
| PermissionsStartOnly=true | ||||
|  | ||||
| # user to run as + where the TLS certificate is (if any) | ||||
| User=cpp | ||||
| Environment=XDG_CONFIG_HOME=/home/cpp/.config | ||||
| ## user to run as + where the TLS certificate is (if any) | ||||
| ## | ||||
| User=copyparty | ||||
| Group=copyparty | ||||
| WorkingDirectory=/var/lib/copyparty | ||||
| Environment=XDG_CONFIG_HOME=/var/lib/copyparty/.config | ||||
|  | ||||
| # 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 | ||||
| ## OPTIONAL: allow copyparty to listen on low ports (like 80/443); | ||||
| ##   you need to uncomment the "p: 80,443,3923" in the config too | ||||
| ##   ------------------------------------------------------------ | ||||
| ##   a slightly safer alternative is to enable partyalone.service | ||||
| ##   which does portforwarding with nftables instead, but an even | ||||
| ##   better option is to use a reverse-proxy (nginx/caddy/...) | ||||
| ## | ||||
| AmbientCapabilities=CAP_NET_BIND_SERVICE | ||||
|  | ||||
| # 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' | ||||
| ## some quick hardening; TODO port more from the nixos package | ||||
| ## | ||||
| MemoryMax=50% | ||||
| MemorySwapMax=50% | ||||
| ProtectClock=true | ||||
| ProtectControlGroups=true | ||||
| ProtectHostname=true | ||||
| ProtectKernelLogs=true | ||||
| ProtectKernelModules=true | ||||
| ProtectKernelTunables=true | ||||
| ProtectProc=invisible | ||||
| RemoveIPC=true | ||||
| RestrictNamespaces=true | ||||
| RestrictRealtime=true | ||||
| RestrictSUIDSGID=true | ||||
|  | ||||
| # copyparty settings | ||||
| ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw | ||||
| ## create a directory for logfiles; | ||||
| ##   this defines $LOGS_DIRECTORY which is used in copyparty.conf | ||||
| ## | ||||
| LogsDirectory=copyparty | ||||
|  | ||||
| ## finally, start copyparty and give it the config file: | ||||
| ## | ||||
| ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -c /etc/copyparty.conf | ||||
|  | ||||
| # NOTE: if you installed copyparty from an OS package repo (nice) | ||||
| #   then you probably want something like this instead: | ||||
| #ExecStart=/usr/bin/copyparty -c /etc/copyparty.conf | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
|   | ||||
| @@ -1,17 +1,22 @@ | ||||
| # this will start `/usr/local/bin/copyparty-sfx.py` | ||||
| # in a chroot, preventing accidental access elsewhere | ||||
| # 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 `cpp` and `--` | ||||
| # | ||||
| # `cpp cpp` = user/group to run copyparty as; can be IDs (1000 1000) | ||||
| # | ||||
| # you may want to: | ||||
| #   change '/mnt::rw' to another location or permission-set | ||||
| #    (remember to change the '/mnt' chroot arg too) | ||||
| # | ||||
| # enable line-buffering for realtime logging (slight performance cost): | ||||
| #   inside the [Service] block, add the following line: | ||||
| # 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] | ||||
| @@ -19,8 +24,17 @@ Description=copyparty file server | ||||
|  | ||||
| [Service] | ||||
| SyslogIdentifier=prisonparty | ||||
| WorkingDirectory=/usr/local/bin | ||||
| ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \ | ||||
| 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 cpp cpp \ | ||||
|   /mnt \ | ||||
|   -- \ | ||||
|   /usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw | ||||
|  | ||||
| [Install] | ||||
|   | ||||
| @@ -3,8 +3,6 @@ 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 | ||||
|  | ||||
| setlocal enabledelayedexpansion | ||||
|  | ||||
| net session >nul 2>&1 | ||||
| if %errorlevel% neq 0 ( | ||||
|     echo sorry, you must run this as administrator | ||||
| @@ -20,30 +18,26 @@ 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 | ||||
| set c=. | ||||
| set /p "c=(Y/N): " | ||||
| echo( | ||||
| if /i not "!c!"=="y" goto :g1 | ||||
| reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\WebClient\Parameters /v BasicAuthLevel /t REG_DWORD /d 0x2 /f | ||||
| rem default is 1 (require tls) | ||||
| 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) | ||||
| ) | ||||
|  | ||||
| :g1 | ||||
| echo( | ||||
| echo OK; | ||||
| echo do you want to disable wpad? | ||||
| echo can give a HUGE speed boost depending on network settings | ||||
| set c=. | ||||
| set /p "c=(Y/N): " | ||||
| echo( | ||||
| if /i not "!c!"=="y" goto :g2 | ||||
| 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 | ||||
| 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 | ||||
| ) | ||||
|  | ||||
| :g2 | ||||
| net stop webclient | ||||
| net start webclient | ||||
| echo( | ||||
|   | ||||
							
								
								
									
										2
									
								
								contrib/windows/copyparty-ctmp.bat
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								contrib/windows/copyparty-ctmp.bat
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| rem run copyparty.exe on machines with busted environment variables | ||||
| cmd /v /c "set TMP=\tmp && copyparty.exe" | ||||
| @@ -6,6 +6,10 @@ import platform | ||||
| import sys | ||||
| import time | ||||
|  | ||||
| # fmt: off | ||||
| _: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 :-)  ************************************************************************************************************************************************ | ||||
| # fmt: on | ||||
|  | ||||
| try: | ||||
|     from typing import TYPE_CHECKING | ||||
| except: | ||||
| @@ -19,7 +23,7 @@ if not PY2: | ||||
|     unicode: Callable[[Any], str] = str | ||||
| else: | ||||
|     sys.dont_write_bytecode = True | ||||
|     unicode = unicode  # noqa: F821  # pylint: disable=undefined-variable,self-assigning-variable | ||||
|     unicode = unicode  # type: ignore | ||||
|  | ||||
| WINDOWS: Any = ( | ||||
|     [int(x) for x in platform.version().split(".")] | ||||
| @@ -27,13 +31,20 @@ WINDOWS: Any = ( | ||||
|     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 | ||||
|  | ||||
| ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"] | ||||
|  | ||||
| MACOS = platform.system() == "Darwin" | ||||
|  | ||||
| EXE = bool(getattr(sys, "frozen", False)) | ||||
|  | ||||
| try: | ||||
|     CORES = len(os.sched_getaffinity(0)) | ||||
| except: | ||||
| @@ -45,7 +56,6 @@ class EnvParams(object): | ||||
|         self.t0 = time.time() | ||||
|         self.mod = "" | ||||
|         self.cfg = "" | ||||
|         self.ox = getattr(sys, "oxidized", None) | ||||
|  | ||||
|  | ||||
| E = EnvParams() | ||||
|   | ||||
							
								
								
									
										970
									
								
								copyparty/__main__.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										970
									
								
								copyparty/__main__.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,8 +1,8 @@ | ||||
| # coding: utf-8 | ||||
|  | ||||
| VERSION = (1, 6, 1) | ||||
| CODENAME = "cors k" | ||||
| BUILD_DT = (2023, 1, 29) | ||||
| VERSION = (1, 13, 2) | ||||
| CODENAME = "race the beam" | ||||
| BUILD_DT = (2024, 5, 10) | ||||
|  | ||||
| S_VERSION = ".".join(map(str, VERSION)) | ||||
| S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) | ||||
|   | ||||
							
								
								
									
										1512
									
								
								copyparty/authsrv.py
									
									
									
									
									
								
							
							
						
						
									
										1512
									
								
								copyparty/authsrv.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -43,6 +43,10 @@ def open(p: str, *a, **ka) -> int: | ||||
|     return os.open(fsenc(p), *a, **ka) | ||||
|  | ||||
|  | ||||
| def readlink(p: str) -> str: | ||||
|     return fsdec(os.readlink(fsenc(p))) | ||||
|  | ||||
|  | ||||
| def rename(src: str, dst: str) -> None: | ||||
|     return os.rename(fsenc(src), fsenc(dst)) | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import queue | ||||
|  | ||||
| from .__init__ import CORES, TYPE_CHECKING | ||||
| from .broker_mpw import MpWorker | ||||
| from .broker_util import try_exec | ||||
| from .broker_util import ExceptionalQueue, try_exec | ||||
| from .util import Daemon, mp | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @@ -46,8 +46,8 @@ class BrokerMp(object): | ||||
|         self.num_workers = self.args.j or CORES | ||||
|         self.log("broker", "booting {} subprocesses".format(self.num_workers)) | ||||
|         for n in range(1, self.num_workers + 1): | ||||
|             q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1) | ||||
|             q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64) | ||||
|             q_pend: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(1)  # type: ignore | ||||
|             q_yield: queue.Queue[tuple[int, str, list[Any]]] = mp.Queue(64)  # type: ignore | ||||
|  | ||||
|             proc = MProcess(q_pend, q_yield, MpWorker, (q_pend, q_yield, self.args, n)) | ||||
|             Daemon(self.collector, "mp-sink-{}".format(n), (proc,)) | ||||
| @@ -57,11 +57,8 @@ class BrokerMp(object): | ||||
|     def shutdown(self) -> None: | ||||
|         self.log("broker", "shutting down") | ||||
|         for n, proc in enumerate(self.procs): | ||||
|             thr = threading.Thread( | ||||
|                 target=proc.q_pend.put((0, "shutdown", [])), | ||||
|                 name="mp-shutdown-{}-{}".format(n, len(self.procs)), | ||||
|             ) | ||||
|             thr.start() | ||||
|             name = "mp-shut-%d-%d" % (n, len(self.procs)) | ||||
|             Daemon(proc.q_pend.put, name, ((0, "shutdown", []),)) | ||||
|  | ||||
|         with self.mutex: | ||||
|             procs = self.procs | ||||
| @@ -69,7 +66,7 @@ class BrokerMp(object): | ||||
|  | ||||
|         while procs: | ||||
|             if procs[-1].is_alive(): | ||||
|                 time.sleep(0.1) | ||||
|                 time.sleep(0.05) | ||||
|                 continue | ||||
|  | ||||
|             procs.pop() | ||||
| @@ -107,6 +104,19 @@ class BrokerMp(object): | ||||
|                 if retq_id: | ||||
|                     proc.q_pend.put((retq_id, "retq", rv)) | ||||
|  | ||||
|     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, | ||||
|   | ||||
| @@ -76,7 +76,7 @@ class MpWorker(BrokerCli): | ||||
|         pass | ||||
|  | ||||
|     def logw(self, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         self.log("mp{}".format(self.n), msg, c) | ||||
|         self.log("mp%d" % (self.n,), msg, c) | ||||
|  | ||||
|     def main(self) -> None: | ||||
|         while True: | ||||
|   | ||||
							
								
								
									
										240
									
								
								copyparty/cert.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								copyparty/cert.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| import calendar | ||||
| import errno | ||||
| import filecmp | ||||
| import json | ||||
| import os | ||||
| import shutil | ||||
| import time | ||||
|  | ||||
| from .__init__ import ANYWIN | ||||
| from .util import Netdev, runcmd, wrename, wunlink | ||||
|  | ||||
| HAVE_CFSSL = True | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from .util import RootLogger | ||||
|  | ||||
|  | ||||
| if ANYWIN: | ||||
|     VF = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1} | ||||
| else: | ||||
|     VF = {"mv_re_t": 0, "rm_re_t": 0} | ||||
|  | ||||
|  | ||||
| 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") | ||||
|     try: | ||||
|         wunlink(log, bname + ".key", VF) | ||||
|     except: | ||||
|         pass | ||||
|     wrename(log, bname + "-key.pem", bname + ".key", VF) | ||||
|     wunlink(log, bname + ".csr", VF) | ||||
|  | ||||
|     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") | ||||
|         if "sans" not in inf: | ||||
|             raise Exception("no useable cert found") | ||||
|  | ||||
|         expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.5 > 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: | ||||
|         wunlink(log, bname + ".key", VF) | ||||
|     except: | ||||
|         pass | ||||
|     wrename(log, bname + "-key.pem", bname + ".key", VF) | ||||
|     wunlink(log, bname + ".csr", VF) | ||||
|  | ||||
|     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) | ||||
							
								
								
									
										241
									
								
								copyparty/cfg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								copyparty/cfg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| # 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 = { | ||||
|         "dav_auth": "davauth", | ||||
|         "dav_rt": "davrt", | ||||
|         "ed": "dots", | ||||
|         "never_symlink": "neversymlink", | ||||
|         "no_dedup": "copydupes", | ||||
|         "no_dupe": "nodupe", | ||||
|         "no_forget": "noforget", | ||||
|         "no_pipe": "nopipe", | ||||
|         "no_robots": "norobots", | ||||
|         "no_thumb": "dthumb", | ||||
|         "no_vthumb": "dvthumb", | ||||
|         "no_athumb": "dathumb", | ||||
|     } | ||||
|     for k in ( | ||||
|         "dotsrch", | ||||
|         "e2d", | ||||
|         "e2ds", | ||||
|         "e2dsa", | ||||
|         "e2t", | ||||
|         "e2ts", | ||||
|         "e2tsr", | ||||
|         "e2v", | ||||
|         "e2vu", | ||||
|         "e2vp", | ||||
|         "exp", | ||||
|         "grid", | ||||
|         "hardlink", | ||||
|         "magic", | ||||
|         "no_sb_md", | ||||
|         "no_sb_lg", | ||||
|         "og", | ||||
|         "og_no_head", | ||||
|         "og_s_title", | ||||
|         "rand", | ||||
|         "xdev", | ||||
|         "xlink", | ||||
|         "xvol", | ||||
|     ): | ||||
|         ret[k] = k | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def vf_vmap() -> dict[str, str]: | ||||
|     """argv-to-volflag: simple values""" | ||||
|     ret = { | ||||
|         "no_hash": "nohash", | ||||
|         "no_idx": "noidx", | ||||
|         "re_maxage": "scan", | ||||
|         "th_convt": "convt", | ||||
|         "th_size": "thsize", | ||||
|         "th_crop": "crop", | ||||
|         "th_x3": "th3x", | ||||
|     } | ||||
|     for k in ( | ||||
|         "dbd", | ||||
|         "html_head", | ||||
|         "lg_sbf", | ||||
|         "md_sbf", | ||||
|         "nrand", | ||||
|         "og_desc", | ||||
|         "og_site", | ||||
|         "og_th", | ||||
|         "og_title", | ||||
|         "og_title_a", | ||||
|         "og_title_v", | ||||
|         "og_title_i", | ||||
|         "og_tpl", | ||||
|         "og_ua", | ||||
|         "mv_retry", | ||||
|         "rm_retry", | ||||
|         "sort", | ||||
|         "tcolor", | ||||
|         "unlist", | ||||
|         "u2abort", | ||||
|         "u2ts", | ||||
|     ): | ||||
|         ret[k] = k | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def vf_cmap() -> dict[str, str]: | ||||
|     """argv-to-volflag: complex/lists""" | ||||
|     ret = {} | ||||
|     for k in ( | ||||
|         "exp_lg", | ||||
|         "exp_md", | ||||
|         "mte", | ||||
|         "mth", | ||||
|         "mtp", | ||||
|         "xad", | ||||
|         "xar", | ||||
|         "xau", | ||||
|         "xban", | ||||
|         "xbd", | ||||
|         "xbr", | ||||
|         "xbu", | ||||
|         "xiu", | ||||
|         "xm", | ||||
|     ): | ||||
|         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", | ||||
|     ".": "dots; user can ask to show dotfiles in listings", | ||||
|     "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", | ||||
|     "A": "all; same as 'rwmda.' (read/write/move/delete/dotfiles)", | ||||
| } | ||||
|  | ||||
|  | ||||
| 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", | ||||
|         "sparse": "force use of sparse files, mainly for s3-backed storage", | ||||
|         "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", | ||||
|         "u2ts=fc": "[f]orce [c]lient-last-modified or [u]pload-time", | ||||
|         "u2abort=1": "allow aborting unfinished uploads? 0=no 1=strict 2=ip-chk 3=acct-chk", | ||||
|         "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", | ||||
|         "pngquant": "compress audio waveforms 33% better", | ||||
|         "thsize": "thumbnail res; WxH", | ||||
|         "crop": "center-cropping (y/n/fy/fn)", | ||||
|         "th3x": "3x resolution (y/n/fy/fn)", | ||||
|         "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", | ||||
|         "sort": "default sort order", | ||||
|         "unlist": "dont list files matching REGEX", | ||||
|         "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH", | ||||
|         "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": { | ||||
|         "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings", | ||||
|         "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', | ||||
|         "fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers', | ||||
|         "mv_retry": "ms-windows: timeout for renaming busy files", | ||||
|         "rm_retry": "ms-windows: timeout for deleting busy files", | ||||
|         "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()} | ||||
| @@ -1,6 +1,7 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import os | ||||
| import re | ||||
| import time | ||||
| @@ -17,20 +18,26 @@ if True:  # pylint: disable=using-constant-test | ||||
|  | ||||
|  | ||||
| class Fstab(object): | ||||
|     def __init__(self, log: "RootLogger"): | ||||
|     def __init__(self, log: "RootLogger", args: argparse.Namespace): | ||||
|         self.log_func = log | ||||
|  | ||||
|         self.warned = False | ||||
|         self.trusted = False | ||||
|         self.tab: Optional[VFS] = None | ||||
|         self.oldtab: Optional[VFS] = None | ||||
|         self.srctab = "a" | ||||
|         self.cache: dict[str, str] = {} | ||||
|         self.age = 0.0 | ||||
|         self.maxage = args.mtab_age | ||||
|  | ||||
|     def log(self, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         self.log_func("fstab", msg, c) | ||||
|  | ||||
|     def get(self, path: str) -> str: | ||||
|         if len(self.cache) > 9000: | ||||
|             self.age = time.time() | ||||
|         now = time.time() | ||||
|         if now - self.age > self.maxage or len(self.cache) > 9000: | ||||
|             self.age = now | ||||
|             self.oldtab = self.tab or self.oldtab | ||||
|             self.tab = None | ||||
|             self.cache = {} | ||||
|  | ||||
| @@ -75,7 +82,7 @@ class Fstab(object): | ||||
|         self.trusted = False | ||||
|  | ||||
|     def build_tab(self) -> None: | ||||
|         self.log("building tab") | ||||
|         self.log("inspecting mtab for changes") | ||||
|  | ||||
|         sptn = r"^.*? on (.*) type ([^ ]+) \(.*" | ||||
|         if MACOS: | ||||
| @@ -84,6 +91,7 @@ class Fstab(object): | ||||
|         ptn = re.compile(sptn) | ||||
|         so, _ = chkcmd(["mount"]) | ||||
|         tab1: list[tuple[str, str]] = [] | ||||
|         atab = [] | ||||
|         for ln in so.split("\n"): | ||||
|             m = ptn.match(ln) | ||||
|             if not m: | ||||
| @@ -91,6 +99,15 @@ class Fstab(object): | ||||
|  | ||||
|             zs1, zs2 = m.groups() | ||||
|             tab1.append((str(zs1), str(zs2))) | ||||
|             atab.append(ln) | ||||
|  | ||||
|         # keep empirically-correct values if mounttab unchanged | ||||
|         srctab = "\n".join(sorted(atab)) | ||||
|         if srctab == self.srctab: | ||||
|             self.tab = self.oldtab | ||||
|             return | ||||
|  | ||||
|         self.log("mtab has changed; reevaluating support for sparse files") | ||||
|  | ||||
|         tab1.sort(key=lambda x: (len(x[0]), x[0])) | ||||
|         path1, fs1 = tab1[0] | ||||
| @@ -99,6 +116,7 @@ class Fstab(object): | ||||
|             tab.add(fs, path.lstrip("/")) | ||||
|  | ||||
|         self.tab = tab | ||||
|         self.srctab = srctab | ||||
|  | ||||
|     def relabel(self, path: str, nval: str) -> None: | ||||
|         assert self.tab | ||||
| @@ -133,7 +151,9 @@ class Fstab(object): | ||||
|                 self.trusted = True | ||||
|             except: | ||||
|                 # prisonparty or other restrictive environment | ||||
|                 self.log("failed to build tab:\n{}".format(min_ex()), 3) | ||||
|                 if not self.warned: | ||||
|                     self.warned = True | ||||
|                     self.log("failed to build tab:\n{}".format(min_ex()), 3) | ||||
|                 self.build_fallback() | ||||
|  | ||||
|         assert self.tab | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import errno | ||||
| import logging | ||||
| import os | ||||
| import stat | ||||
| @@ -11,20 +12,25 @@ import time | ||||
| from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer | ||||
| from pyftpdlib.filesystems import AbstractedFS, FilesystemError | ||||
| from pyftpdlib.handlers import FTPHandler | ||||
| from pyftpdlib.ioloop import IOLoop | ||||
| from pyftpdlib.servers import FTPServer | ||||
|  | ||||
| from .__init__ import PY2, TYPE_CHECKING, E | ||||
| from .__init__ import PY2, TYPE_CHECKING | ||||
| from .authsrv import VFS | ||||
| from .bos import bos | ||||
| from .util import Daemon, Pebkac, exclude_dotfiles, fsenc, ipnorm | ||||
|  | ||||
| try: | ||||
|     from pyftpdlib.ioloop import IOLoop | ||||
| except ImportError: | ||||
|     p = os.path.join(E.mod, "vend") | ||||
|     print("loading asynchat from " + p) | ||||
|     sys.path.append(p) | ||||
|     from pyftpdlib.ioloop import IOLoop | ||||
|  | ||||
| from .util import ( | ||||
|     Daemon, | ||||
|     ODict, | ||||
|     Pebkac, | ||||
|     exclude_dotfiles, | ||||
|     fsenc, | ||||
|     ipnorm, | ||||
|     pybin, | ||||
|     relchk, | ||||
|     runhook, | ||||
|     sanitize_fn, | ||||
|     vjoin, | ||||
| ) | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .svchub import SvcHub | ||||
| @@ -34,6 +40,12 @@ if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Any, Optional | ||||
|  | ||||
|  | ||||
| class FSE(FilesystemError): | ||||
|     def __init__(self, msg: str, severity: int = 0) -> None: | ||||
|         super(FilesystemError, self).__init__(msg) | ||||
|         self.severity = severity | ||||
|  | ||||
|  | ||||
| class FtpAuth(DummyAuthorizer): | ||||
|     def __init__(self, hub: "SvcHub") -> None: | ||||
|         super(FtpAuth, self).__init__() | ||||
| @@ -43,6 +55,7 @@ class FtpAuth(DummyAuthorizer): | ||||
|         self, username: str, password: str, handler: Any | ||||
|     ) -> None: | ||||
|         handler.username = "{}:{}".format(username, password) | ||||
|         handler.uname = "*" | ||||
|  | ||||
|         ip = handler.addr[0] | ||||
|         if ip.startswith("::ffff:"): | ||||
| @@ -59,10 +72,14 @@ class FtpAuth(DummyAuthorizer): | ||||
|                 raise AuthenticationFailed("banned") | ||||
|  | ||||
|         asrv = self.hub.asrv | ||||
|         if username == "anonymous": | ||||
|             uname = "*" | ||||
|         else: | ||||
|             uname = asrv.iacct.get(password, "") or asrv.iacct.get(username, "") or "*" | ||||
|         uname = "*" | ||||
|         if username != "anonymous": | ||||
|             uname = "" | ||||
|             for zs in (password, username): | ||||
|                 zs = asrv.iacct.get(asrv.ah.hash(zs), "") | ||||
|                 if zs: | ||||
|                     uname = zs | ||||
|                     break | ||||
|  | ||||
|         if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): | ||||
|             g = self.hub.gpwd | ||||
| @@ -71,17 +88,23 @@ class FtpAuth(DummyAuthorizer): | ||||
|                 if bonk: | ||||
|                     logging.warning("client banned: invalid passwords") | ||||
|                     bans[ip] = bonk | ||||
|                     try: | ||||
|                         # only possible if multiprocessing disabled | ||||
|                         self.hub.broker.httpsrv.bans[ip] = bonk  # type: ignore | ||||
|                         self.hub.broker.httpsrv.nban += 1  # type: ignore | ||||
|                     except: | ||||
|                         pass | ||||
|  | ||||
|             raise AuthenticationFailed("Authentication failed.") | ||||
|  | ||||
|         handler.username = uname | ||||
|         handler.uname = handler.username = uname | ||||
|  | ||||
|     def get_home_dir(self, username: str) -> str: | ||||
|         return "/" | ||||
|  | ||||
|     def has_user(self, username: str) -> bool: | ||||
|         asrv = self.hub.asrv | ||||
|         return username in asrv.acct | ||||
|         return username in asrv.acct or username in asrv.iacct | ||||
|  | ||||
|     def has_perm(self, username: str, perm: int, path: Optional[str] = None) -> bool: | ||||
|         return True  # handled at filesystem layer | ||||
| @@ -100,17 +123,18 @@ class FtpFs(AbstractedFS): | ||||
|     def __init__( | ||||
|         self, root: str, cmd_channel: Any | ||||
|     ) -> None:  # pylint: disable=super-init-not-called | ||||
|         self.h = self.cmd_channel = cmd_channel  # type: FTPHandler | ||||
|         self.h = cmd_channel  # type: FTPHandler | ||||
|         self.cmd_channel = cmd_channel  # type: FTPHandler | ||||
|         self.hub: "SvcHub" = cmd_channel.hub | ||||
|         self.args = cmd_channel.args | ||||
|  | ||||
|         self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*") | ||||
|         self.uname = cmd_channel.uname | ||||
|  | ||||
|         self.cwd = "/"  # pyftpdlib convention of leading slash | ||||
|         self.root = "/var/lib/empty" | ||||
|  | ||||
|         self.can_read = self.can_write = self.can_move = False | ||||
|         self.can_delete = self.can_get = self.can_upget = False | ||||
|         self.can_admin = self.can_dot = False | ||||
|  | ||||
|         self.listdirinfo = self.listdir | ||||
|         self.chdir(".") | ||||
| @@ -122,16 +146,36 @@ class FtpFs(AbstractedFS): | ||||
|         w: bool = False, | ||||
|         m: bool = False, | ||||
|         d: bool = False, | ||||
|     ) -> str: | ||||
|     ) -> tuple[str, VFS, str]: | ||||
|         try: | ||||
|             vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|             vpath = vpath.replace("\\", "/").strip("/") | ||||
|             rd, fn = os.path.split(vpath) | ||||
|             if relchk(rd): | ||||
|                 logging.warning("malicious vpath: %s", vpath) | ||||
|                 t = "Unsupported characters in [{}]" | ||||
|                 raise FSE(t.format(vpath), 1) | ||||
|  | ||||
|             fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"]) | ||||
|             vpath = vjoin(rd, fn) | ||||
|             vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d) | ||||
|             if not vfs.realpath: | ||||
|                 raise FilesystemError("no filesystem mounted at this path") | ||||
|                 t = "No filesystem mounted at [{}]" | ||||
|                 raise FSE(t.format(vpath)) | ||||
|  | ||||
|             return os.path.join(vfs.realpath, rem) | ||||
|             if "xdev" in vfs.flags or "xvol" in vfs.flags: | ||||
|                 ap = vfs.canonical(rem) | ||||
|                 avfs = vfs.chk_ap(ap) | ||||
|                 t = "Permission denied in [{}]" | ||||
|                 if not avfs: | ||||
|                     raise FSE(t.format(vpath), 1) | ||||
|  | ||||
|                 cr, cw, cm, cd, _, _, _, _ = avfs.can_access("", self.h.uname) | ||||
|                 if r and not cr or w and not cw or m and not cm or d and not cd: | ||||
|                     raise FSE(t.format(vpath), 1) | ||||
|  | ||||
|             return os.path.join(vfs.realpath, rem), vfs, rem | ||||
|         except Pebkac as ex: | ||||
|             raise FilesystemError(str(ex)) | ||||
|             raise FSE(str(ex)) | ||||
|  | ||||
|     def rv2a( | ||||
|         self, | ||||
| @@ -140,7 +184,7 @@ class FtpFs(AbstractedFS): | ||||
|         w: bool = False, | ||||
|         m: bool = False, | ||||
|         d: bool = False, | ||||
|     ) -> str: | ||||
|     ) -> tuple[str, VFS, str]: | ||||
|         return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d) | ||||
|  | ||||
|     def ftp2fs(self, ftppath: str) -> str: | ||||
| @@ -154,7 +198,7 @@ class FtpFs(AbstractedFS): | ||||
|     def validpath(self, path: str) -> bool: | ||||
|         if "/.hist/" in path: | ||||
|             if "/up2k." in path or path.endswith("/dir.txt"): | ||||
|                 raise FilesystemError("access to this file is forbidden") | ||||
|                 raise FSE("Access to this file is forbidden", 1) | ||||
|  | ||||
|         return True | ||||
|  | ||||
| @@ -162,7 +206,7 @@ class FtpFs(AbstractedFS): | ||||
|         r = "r" in mode | ||||
|         w = "w" in mode or "a" in mode or "+" in mode | ||||
|  | ||||
|         ap = self.rv2a(filename, r, w) | ||||
|         ap = self.rv2a(filename, r, w)[0] | ||||
|         if w: | ||||
|             try: | ||||
|                 st = bos.stat(ap) | ||||
| @@ -171,18 +215,26 @@ class FtpFs(AbstractedFS): | ||||
|                 td = 0 | ||||
|  | ||||
|             if td < -1 or td > self.args.ftp_wt: | ||||
|                 raise FilesystemError("cannot open existing file for writing") | ||||
|                 raise FSE("Cannot open existing file for writing") | ||||
|  | ||||
|         self.validpath(ap) | ||||
|         return open(fsenc(ap), mode) | ||||
|         return open(fsenc(ap), mode, self.args.iobuf) | ||||
|  | ||||
|     def chdir(self, path: str) -> None: | ||||
|         nwd = join(self.cwd, path) | ||||
|         vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False) | ||||
|         ap = vfs.canonical(rem) | ||||
|         if not bos.path.isdir(ap): | ||||
|         try: | ||||
|             st = bos.stat(ap) | ||||
|             if not stat.S_ISDIR(st.st_mode): | ||||
|                 raise Exception() | ||||
|         except: | ||||
|             # returning 550 is library-default and suitable | ||||
|             raise FilesystemError("Failed to change directory") | ||||
|             raise FSE("No such file or directory") | ||||
|  | ||||
|         avfs = vfs.chk_ap(ap, st) | ||||
|         if not avfs: | ||||
|             raise FSE("Permission denied", 1) | ||||
|  | ||||
|         self.cwd = nwd | ||||
|         ( | ||||
| @@ -192,16 +244,20 @@ class FtpFs(AbstractedFS): | ||||
|             self.can_delete, | ||||
|             self.can_get, | ||||
|             self.can_upget, | ||||
|         ) = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.username) | ||||
|             self.can_admin, | ||||
|             self.can_dot, | ||||
|         ) = avfs.can_access("", self.h.uname) | ||||
|  | ||||
|     def mkdir(self, path: str) -> None: | ||||
|         ap = self.rv2a(path, w=True) | ||||
|         bos.mkdir(ap) | ||||
|         ap = self.rv2a(path, w=True)[0] | ||||
|         bos.makedirs(ap)  # filezilla expects this | ||||
|  | ||||
|     def listdir(self, path: str) -> list[str]: | ||||
|         vpath = join(self.cwd, path).lstrip("/") | ||||
|         vpath = join(self.cwd, path) | ||||
|         try: | ||||
|             vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False) | ||||
|             ap, vfs, rem = self.v2a(vpath, True, False) | ||||
|             if not bos.path.isdir(ap): | ||||
|                 raise FSE("No such file or directory", 1) | ||||
|  | ||||
|             fsroot, vfs_ls1, vfs_virt = vfs.ls( | ||||
|                 rem, | ||||
| @@ -212,13 +268,17 @@ class FtpFs(AbstractedFS): | ||||
|             vfs_ls = [x[0] for x in vfs_ls1] | ||||
|             vfs_ls.extend(vfs_virt.keys()) | ||||
|  | ||||
|             if not self.args.ed: | ||||
|             if not self.can_dot: | ||||
|                 vfs_ls = exclude_dotfiles(vfs_ls) | ||||
|  | ||||
|             vfs_ls.sort() | ||||
|             return vfs_ls | ||||
|         except: | ||||
|             if vpath: | ||||
|         except Exception as ex: | ||||
|             # panic on malicious names | ||||
|             if getattr(ex, "severity", 0): | ||||
|                 raise | ||||
|  | ||||
|             if vpath.strip("/"): | ||||
|                 # display write-only folders as empty | ||||
|                 return [] | ||||
|  | ||||
| @@ -227,43 +287,49 @@ class FtpFs(AbstractedFS): | ||||
|             return list(sorted(list(r.keys()))) | ||||
|  | ||||
|     def rmdir(self, path: str) -> None: | ||||
|         ap = self.rv2a(path, d=True) | ||||
|         bos.rmdir(ap) | ||||
|         ap = self.rv2a(path, d=True)[0] | ||||
|         try: | ||||
|             bos.rmdir(ap) | ||||
|         except OSError as e: | ||||
|             if e.errno != errno.ENOENT: | ||||
|                 raise | ||||
|  | ||||
|     def remove(self, path: str) -> None: | ||||
|         if self.args.no_del: | ||||
|             raise FilesystemError("the delete feature is disabled in server config") | ||||
|             raise FSE("The delete feature is disabled in server config") | ||||
|  | ||||
|         vp = join(self.cwd, path).lstrip("/") | ||||
|         try: | ||||
|             self.hub.up2k.handle_rm(self.uname, self.h.remote_ip, [vp], []) | ||||
|             self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False, False) | ||||
|         except Exception as ex: | ||||
|             raise FilesystemError(str(ex)) | ||||
|             raise FSE(str(ex)) | ||||
|  | ||||
|     def rename(self, src: str, dst: str) -> None: | ||||
|         if not self.can_move: | ||||
|             raise FilesystemError("not allowed for user " + self.h.username) | ||||
|             raise FSE("Not allowed for user " + self.h.uname) | ||||
|  | ||||
|         if self.args.no_mv: | ||||
|             t = "the rename/move feature is disabled in server config" | ||||
|             raise FilesystemError(t) | ||||
|             raise FSE("The rename/move feature is disabled in server config") | ||||
|  | ||||
|         svp = join(self.cwd, src).lstrip("/") | ||||
|         dvp = join(self.cwd, dst).lstrip("/") | ||||
|         try: | ||||
|             self.hub.up2k.handle_mv(self.uname, svp, dvp) | ||||
|         except Exception as ex: | ||||
|             raise FilesystemError(str(ex)) | ||||
|             raise FSE(str(ex)) | ||||
|  | ||||
|     def chmod(self, path: str, mode: str) -> None: | ||||
|         pass | ||||
|  | ||||
|     def stat(self, path: str) -> os.stat_result: | ||||
|         try: | ||||
|             ap = self.rv2a(path, r=True) | ||||
|             ap = self.rv2a(path, r=True)[0] | ||||
|             return bos.stat(ap) | ||||
|         except: | ||||
|             ap = self.rv2a(path) | ||||
|         except FSE as ex: | ||||
|             if ex.severity: | ||||
|                 raise | ||||
|  | ||||
|             ap = self.rv2a(path)[0] | ||||
|             st = bos.stat(ap) | ||||
|             if not stat.S_ISDIR(st.st_mode): | ||||
|                 raise | ||||
| @@ -271,44 +337,50 @@ class FtpFs(AbstractedFS): | ||||
|             return st | ||||
|  | ||||
|     def utime(self, path: str, timeval: float) -> None: | ||||
|         ap = self.rv2a(path, w=True) | ||||
|         ap = self.rv2a(path, w=True)[0] | ||||
|         return bos.utime(ap, (timeval, timeval)) | ||||
|  | ||||
|     def lstat(self, path: str) -> os.stat_result: | ||||
|         ap = self.rv2a(path) | ||||
|         ap = self.rv2a(path)[0] | ||||
|         return bos.stat(ap) | ||||
|  | ||||
|     def isfile(self, path: str) -> bool: | ||||
|         try: | ||||
|             st = self.stat(path) | ||||
|             return stat.S_ISREG(st.st_mode) | ||||
|         except: | ||||
|         except Exception as ex: | ||||
|             if getattr(ex, "severity", 0): | ||||
|                 raise | ||||
|  | ||||
|             return False  # expected for mojibake in ftp_SIZE() | ||||
|  | ||||
|     def islink(self, path: str) -> bool: | ||||
|         ap = self.rv2a(path) | ||||
|         ap = self.rv2a(path)[0] | ||||
|         return bos.path.islink(ap) | ||||
|  | ||||
|     def isdir(self, path: str) -> bool: | ||||
|         try: | ||||
|             st = self.stat(path) | ||||
|             return stat.S_ISDIR(st.st_mode) | ||||
|         except: | ||||
|         except Exception as ex: | ||||
|             if getattr(ex, "severity", 0): | ||||
|                 raise | ||||
|  | ||||
|             return True | ||||
|  | ||||
|     def getsize(self, path: str) -> int: | ||||
|         ap = self.rv2a(path) | ||||
|         ap = self.rv2a(path)[0] | ||||
|         return bos.path.getsize(ap) | ||||
|  | ||||
|     def getmtime(self, path: str) -> float: | ||||
|         ap = self.rv2a(path) | ||||
|         ap = self.rv2a(path)[0] | ||||
|         return bos.path.getmtime(ap) | ||||
|  | ||||
|     def realpath(self, path: str) -> str: | ||||
|         return path | ||||
|  | ||||
|     def lexists(self, path: str) -> bool: | ||||
|         ap = self.rv2a(path) | ||||
|         ap = self.rv2a(path)[0] | ||||
|         return bos.path.lexists(ap) | ||||
|  | ||||
|     def get_user_by_uid(self, uid: int) -> str: | ||||
| @@ -322,16 +394,30 @@ class FtpHandler(FTPHandler): | ||||
|     abstracted_fs = FtpFs | ||||
|     hub: "SvcHub" | ||||
|     args: argparse.Namespace | ||||
|     uname: str | ||||
|  | ||||
|     def __init__(self, conn: Any, server: Any, ioloop: Any = None) -> None: | ||||
|         self.hub: "SvcHub" = FtpHandler.hub | ||||
|         self.args: argparse.Namespace = FtpHandler.args | ||||
|         self.uname = "*" | ||||
|  | ||||
|         if PY2: | ||||
|             FTPHandler.__init__(self, conn, server, ioloop) | ||||
|         else: | ||||
|             super(FtpHandler, self).__init__(conn, server, ioloop) | ||||
|  | ||||
|         cip = self.remote_ip | ||||
|         if cip.startswith("::ffff:"): | ||||
|             cip = cip[7:] | ||||
|  | ||||
|         if self.args.ftp_ipa_nm and not self.args.ftp_ipa_nm.map(cip): | ||||
|             logging.warning("client rejected (--ftp-ipa): %s", cip) | ||||
|             self.connected = False | ||||
|             conn.close() | ||||
|             return | ||||
|  | ||||
|         self.cli_ip = cip | ||||
|  | ||||
|         # abspath->vpath mapping to resolve log_transfer paths | ||||
|         self.vfs_map: dict[str, str] = {} | ||||
|  | ||||
| @@ -341,8 +427,24 @@ class FtpHandler(FTPHandler): | ||||
|     def ftp_STOR(self, file: str, mode: str = "w") -> Any: | ||||
|         # Optional[str] | ||||
|         vp = join(self.fs.cwd, file).lstrip("/") | ||||
|         ap = self.fs.v2a(vp) | ||||
|         ap, vfs, rem = self.fs.v2a(vp, w=True) | ||||
|         self.vfs_map[ap] = vp | ||||
|         xbu = vfs.flags.get("xbu") | ||||
|         if xbu and not runhook( | ||||
|             None, | ||||
|             xbu, | ||||
|             ap, | ||||
|             vfs.canonical(rem), | ||||
|             "", | ||||
|             self.uname, | ||||
|             0, | ||||
|             0, | ||||
|             self.cli_ip, | ||||
|             0, | ||||
|             "", | ||||
|         ): | ||||
|             raise FSE("Upload blocked by xbu server config") | ||||
|  | ||||
|         # print("ftp_STOR: {} {} => {}".format(vp, mode, ap)) | ||||
|         ret = FTPHandler.ftp_STOR(self, file, mode) | ||||
|         # print("ftp_STOR: {} {} OK".format(vp, mode)) | ||||
| @@ -363,15 +465,17 @@ class FtpHandler(FTPHandler): | ||||
|         # print("xfer_end: {} => {}".format(ap, vp)) | ||||
|         if vp: | ||||
|             vp, fn = os.path.split(vp) | ||||
|             vfs, rem = self.hub.asrv.vfs.get(vp, self.username, False, True) | ||||
|             vfs, rem = self.hub.asrv.vfs.get(vp, self.uname, False, True) | ||||
|             vfs, rem = vfs.get_dbv(rem) | ||||
|             self.hub.up2k.hash_file( | ||||
|                 vfs.realpath, | ||||
|                 vfs.vpath, | ||||
|                 vfs.flags, | ||||
|                 rem, | ||||
|                 fn, | ||||
|                 self.remote_ip, | ||||
|                 self.cli_ip, | ||||
|                 time.time(), | ||||
|                 self.uname, | ||||
|             ) | ||||
|  | ||||
|         return FTPHandler.log_transfer( | ||||
| @@ -402,10 +506,10 @@ class Ftpd(object): | ||||
|                 h1 = SftpHandler | ||||
|             except: | ||||
|                 t = "\nftps requires pyopenssl;\nplease run the following:\n\n  {} -m pip install --user pyopenssl\n" | ||||
|                 print(t.format(sys.executable)) | ||||
|                 print(t.format(pybin)) | ||||
|                 sys.exit(1) | ||||
|  | ||||
|             h1.certfile = os.path.join(self.args.E.cfg, "cert.pem") | ||||
|             h1.certfile = self.args.cert | ||||
|             h1.tls_control_required = True | ||||
|             h1.tls_data_required = True | ||||
|  | ||||
| @@ -413,9 +517,9 @@ class Ftpd(object): | ||||
|  | ||||
|         for h_lp in hs: | ||||
|             h2, lp = h_lp | ||||
|             h2.hub = hub | ||||
|             h2.args = hub.args | ||||
|             h2.authorizer = FtpAuth(hub) | ||||
|             FtpHandler.hub = h2.hub = hub | ||||
|             FtpHandler.args = h2.args = hub.args | ||||
|             FtpHandler.authorizer = h2.authorizer = FtpAuth(hub) | ||||
|  | ||||
|             if self.args.ftp_pr: | ||||
|                 p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")] | ||||
| @@ -435,10 +539,23 @@ class Ftpd(object): | ||||
|         lgr = logging.getLogger("pyftpdlib") | ||||
|         lgr.setLevel(logging.DEBUG if self.args.ftpv else logging.INFO) | ||||
|  | ||||
|         ips = self.args.i | ||||
|         if "::" in ips: | ||||
|             ips.append("0.0.0.0") | ||||
|  | ||||
|         if self.args.ftp4: | ||||
|             ips = [x for x in ips if ":" not in x] | ||||
|  | ||||
|         ips = list(ODict.fromkeys(ips))  # dedup | ||||
|  | ||||
|         ioloop = IOLoop() | ||||
|         for ip in self.args.i: | ||||
|         for ip in ips: | ||||
|             for h, lp in hs: | ||||
|                 FTPServer((ip, int(lp)), h, ioloop) | ||||
|                 try: | ||||
|                     FTPServer((ip, int(lp)), h, ioloop) | ||||
|                 except: | ||||
|                     if ip != "0.0.0.0" or "::" not in ips: | ||||
|                         raise | ||||
|  | ||||
|         Daemon(ioloop.loop, "ftp") | ||||
|  | ||||
|   | ||||
							
								
								
									
										2285
									
								
								copyparty/httpcli.py
									
									
									
									
									
								
							
							
						
						
									
										2285
									
								
								copyparty/httpcli.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -23,7 +23,7 @@ from .mtag import HAVE_FFMPEG | ||||
| from .th_cli import ThumbCli | ||||
| from .th_srv import HAVE_PIL, HAVE_VIPS | ||||
| from .u2idx import U2idx | ||||
| from .util import HMaccas, shut_socket | ||||
| from .util import HMaccas, NetMap, shut_socket | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Optional, Pattern, Union | ||||
| @@ -50,12 +50,15 @@ class HttpConn(object): | ||||
|         self.addr = addr | ||||
|         self.hsrv = hsrv | ||||
|  | ||||
|         self.mutex: threading.Lock = hsrv.mutex  # mypy404 | ||||
|         self.u2mutex: threading.Lock = hsrv.u2mutex  # mypy404 | ||||
|         self.args: argparse.Namespace = hsrv.args  # mypy404 | ||||
|         self.E: EnvParams = self.args.E | ||||
|         self.asrv: AuthSrv = hsrv.asrv  # mypy404 | ||||
|         self.cert_path = hsrv.cert_path | ||||
|         self.u2fh: Util.FHC = hsrv.u2fh  # mypy404 | ||||
|         self.pipes: Util.CachedDict = hsrv.pipes  # mypy404 | ||||
|         self.ipa_nm: Optional[NetMap] = hsrv.ipa_nm | ||||
|         self.xff_nm: Optional[NetMap] = hsrv.xff_nm | ||||
|         self.xff_lan: NetMap = hsrv.xff_lan  # type: ignore | ||||
|         self.iphash: HMaccas = hsrv.broker.iphash | ||||
|         self.bans: dict[str, int] = hsrv.bans | ||||
|         self.aclose: dict[str, int] = hsrv.aclose | ||||
| @@ -94,7 +97,7 @@ class HttpConn(object): | ||||
|             self.rproxy = ip | ||||
|  | ||||
|         self.ip = ip | ||||
|         self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26) | ||||
|         self.log_src = ("%s \033[%dm%d" % (ip, color, self.addr[1])).ljust(26) | ||||
|         return self.log_src | ||||
|  | ||||
|     def respath(self, res_name: str) -> str: | ||||
| @@ -103,41 +106,40 @@ class HttpConn(object): | ||||
|     def log(self, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         self.log_func(self.log_src, msg, c) | ||||
|  | ||||
|     def get_u2idx(self) -> U2idx: | ||||
|         # one u2idx per tcp connection; | ||||
|     def get_u2idx(self) -> Optional[U2idx]: | ||||
|         # grab from a pool of u2idx instances; | ||||
|         # sqlite3 fully parallelizes under python threads | ||||
|         # but avoid running out of FDs by creating too many | ||||
|         if not self.u2idx: | ||||
|             self.u2idx = U2idx(self) | ||||
|             self.u2idx = self.hsrv.get_u2idx(str(self.addr)) | ||||
|  | ||||
|         return self.u2idx | ||||
|  | ||||
|     def _detect_https(self) -> bool: | ||||
|         method = None | ||||
|         if self.cert_path: | ||||
|             try: | ||||
|                 method = self.s.recv(4, socket.MSG_PEEK) | ||||
|             except socket.timeout: | ||||
|                 return False | ||||
|             except AttributeError: | ||||
|                 # jython does not support msg_peek; forget about https | ||||
|                 method = self.s.recv(4) | ||||
|                 self.sr = Util.Unrecv(self.s, self.log) | ||||
|                 self.sr.buf = method | ||||
|         try: | ||||
|             method = self.s.recv(4, socket.MSG_PEEK) | ||||
|         except socket.timeout: | ||||
|             return False | ||||
|         except AttributeError: | ||||
|             # jython does not support msg_peek; forget about https | ||||
|             method = self.s.recv(4) | ||||
|             self.sr = Util.Unrecv(self.s, self.log) | ||||
|             self.sr.buf = method | ||||
|  | ||||
|                 # jython used to do this, they stopped since it's broken | ||||
|                 # but reimplementing sendall is out of scope for now | ||||
|                 if not getattr(self.s, "sendall", None): | ||||
|                     self.s.sendall = self.s.send  # type: ignore | ||||
|             # jython used to do this, they stopped since it's broken | ||||
|             # but reimplementing sendall is out of scope for now | ||||
|             if not getattr(self.s, "sendall", None): | ||||
|                 self.s.sendall = self.s.send  # type: ignore | ||||
|  | ||||
|             if len(method) != 4: | ||||
|                 err = "need at least 4 bytes in the first packet; got {}".format( | ||||
|                     len(method) | ||||
|                 ) | ||||
|                 if method: | ||||
|                     self.log(err) | ||||
|         if len(method) != 4: | ||||
|             err = "need at least 4 bytes in the first packet; got {}".format( | ||||
|                 len(method) | ||||
|             ) | ||||
|             if method: | ||||
|                 self.log(err) | ||||
|  | ||||
|                 self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8")) | ||||
|                 return False | ||||
|             self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8")) | ||||
|             return False | ||||
|  | ||||
|         return not method or not bool(PTN_HTTP.match(method)) | ||||
|  | ||||
| @@ -147,7 +149,7 @@ class HttpConn(object): | ||||
|         self.sr = None | ||||
|         if self.args.https_only: | ||||
|             is_https = True | ||||
|         elif self.args.http_only or not HAVE_SSL: | ||||
|         elif self.args.http_only: | ||||
|             is_https = False | ||||
|         else: | ||||
|             # raise Exception("asdf") | ||||
| @@ -161,7 +163,7 @@ class HttpConn(object): | ||||
|             self.log_src = self.log_src.replace("[36m", "[35m") | ||||
|             try: | ||||
|                 ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) | ||||
|                 ctx.load_cert_chain(self.cert_path) | ||||
|                 ctx.load_cert_chain(self.args.cert) | ||||
|                 if self.args.ssl_ver: | ||||
|                     ctx.options &= ~self.args.ssl_flags_en | ||||
|                     ctx.options |= self.args.ssl_flags_de | ||||
| @@ -178,7 +180,7 @@ class HttpConn(object): | ||||
|  | ||||
|                 self.s = ctx.wrap_socket(self.s, server_side=True) | ||||
|                 msg = [ | ||||
|                     "\033[1;3{:d}m{}".format(c, s) | ||||
|                     "\033[1;3%dm%s" % (c, s) | ||||
|                     for c, s in zip([0, 5, 0], self.s.cipher())  # type: ignore | ||||
|                 ] | ||||
|                 self.log(" ".join(msg) + "\033[0m") | ||||
| @@ -215,3 +217,7 @@ class HttpConn(object): | ||||
|             self.cli = HttpCli(self) | ||||
|             if not self.cli.run(): | ||||
|                 return | ||||
|  | ||||
|             if self.u2idx: | ||||
|                 self.hsrv.put_u2idx(str(self.addr), self.u2idx) | ||||
|                 self.u2idx = None | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals | ||||
| import base64 | ||||
| import math | ||||
| import os | ||||
| import re | ||||
| import socket | ||||
| import sys | ||||
| import threading | ||||
| @@ -11,9 +12,19 @@ import time | ||||
|  | ||||
| import queue | ||||
|  | ||||
| from .__init__ import ANYWIN, CORES, EXE, MACOS, TYPE_CHECKING, EnvParams | ||||
|  | ||||
| try: | ||||
|     MNFE = ModuleNotFoundError | ||||
| except: | ||||
|     MNFE = ImportError | ||||
|  | ||||
| try: | ||||
|     import jinja2 | ||||
| except ImportError: | ||||
| except MNFE: | ||||
|     if EXE: | ||||
|         raise | ||||
|  | ||||
|     print( | ||||
|         """\033[1;31m | ||||
|   you do not have jinja2 installed,\033[33m | ||||
| @@ -23,22 +34,41 @@ except ImportError: | ||||
|    * (try another python version, if you have one) | ||||
|    * (try copyparty.sfx instead) | ||||
| """.format( | ||||
|             os.path.basename(sys.executable) | ||||
|             sys.executable | ||||
|         ) | ||||
|     ) | ||||
|     sys.exit(1) | ||||
| except SyntaxError: | ||||
|     if EXE: | ||||
|         raise | ||||
|  | ||||
|     print( | ||||
|         """\033[1;31m | ||||
|   your jinja2 version is incompatible with your python version;\033[33m | ||||
|   please try to replace it with an older version:\033[0m | ||||
|    * {} -m pip install --user jinja2==2.11.3 | ||||
|    * (try another python version, if you have one) | ||||
|    * (try copyparty.sfx instead) | ||||
| """.format( | ||||
|             sys.executable | ||||
|         ) | ||||
|     ) | ||||
|     sys.exit(1) | ||||
|  | ||||
| from .__init__ import ANYWIN, MACOS, TYPE_CHECKING, EnvParams | ||||
| from .bos import bos | ||||
| from .httpconn import HttpConn | ||||
| from .metrics import Metrics | ||||
| from .u2idx import U2idx | ||||
| from .util import ( | ||||
|     E_SCK, | ||||
|     FHC, | ||||
|     CachedDict, | ||||
|     Daemon, | ||||
|     Garda, | ||||
|     Magician, | ||||
|     Netdev, | ||||
|     NetMap, | ||||
|     absreal, | ||||
|     build_netmap, | ||||
|     ipnorm, | ||||
|     min_ex, | ||||
|     shut_socket, | ||||
| @@ -72,18 +102,24 @@ class HttpSrv(object): | ||||
|         # redefine in case of multiprocessing | ||||
|         socket.setdefaulttimeout(120) | ||||
|  | ||||
|         self.t0 = time.time() | ||||
|         nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else "" | ||||
|         self.magician = Magician() | ||||
|         self.nm = NetMap([], {}) | ||||
|         self.nm = NetMap([], []) | ||||
|         self.ssdp: Optional["SSDPr"] = None | ||||
|         self.gpwd = Garda(self.args.ban_pw) | ||||
|         self.g404 = Garda(self.args.ban_404) | ||||
|         self.g403 = Garda(self.args.ban_403) | ||||
|         self.g422 = Garda(self.args.ban_422, False) | ||||
|         self.gmal = Garda(self.args.ban_422) | ||||
|         self.gurl = Garda(self.args.ban_url) | ||||
|         self.bans: dict[str, int] = {} | ||||
|         self.aclose: dict[str, int] = {} | ||||
|  | ||||
|         self.bound: set[tuple[str, int]] = set() | ||||
|         self.name = "hsrv" + nsuf | ||||
|         self.mutex = threading.Lock() | ||||
|         self.u2mutex = threading.Lock() | ||||
|         self.stopping = False | ||||
|  | ||||
|         self.tp_nthr = 0  # actual | ||||
| @@ -95,6 +131,11 @@ class HttpSrv(object): | ||||
|         self.t_periodic: Optional[threading.Thread] = None | ||||
|  | ||||
|         self.u2fh = FHC() | ||||
|         self.pipes = CachedDict(0.2) | ||||
|         self.metrics = Metrics(self) | ||||
|         self.nreq = 0 | ||||
|         self.nsus = 0 | ||||
|         self.nban = 0 | ||||
|         self.srvs: list[socket.socket] = [] | ||||
|         self.ncli = 0  # exact | ||||
|         self.clients: set[HttpConn] = set()  # laggy | ||||
| @@ -102,6 +143,9 @@ class HttpSrv(object): | ||||
|         self.cb_ts = 0.0 | ||||
|         self.cb_v = "" | ||||
|  | ||||
|         self.u2idx_free: dict[str, U2idx] = {} | ||||
|         self.u2idx_n = 0 | ||||
|  | ||||
|         env = jinja2.Environment() | ||||
|         env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web")) | ||||
|         jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"] | ||||
| @@ -109,6 +153,16 @@ class HttpSrv(object): | ||||
|         zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz") | ||||
|         self.prism = os.path.exists(zs) | ||||
|  | ||||
|         self.ipa_nm = build_netmap(self.args.ipa) | ||||
|         self.xff_nm = build_netmap(self.args.xff_src) | ||||
|         self.xff_lan = build_netmap("lan") | ||||
|  | ||||
|         self.statics: set[str] = set() | ||||
|         self._build_statics() | ||||
|  | ||||
|         self.ptn_cc = re.compile(r"[\x00-\x1f]") | ||||
|         self.ptn_hsafe = re.compile(r"[\x00-\x1f<>\"'&]") | ||||
|  | ||||
|         self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split() | ||||
|         if not self.args.no_dav: | ||||
|             zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE" | ||||
| @@ -119,12 +173,6 @@ class HttpSrv(object): | ||||
|  | ||||
|             self.ssdp = SSDPr(broker) | ||||
|  | ||||
|         cert_path = os.path.join(self.E.cfg, "cert.pem") | ||||
|         if bos.path.exists(cert_path): | ||||
|             self.cert_path = cert_path | ||||
|         else: | ||||
|             self.cert_path = "" | ||||
|  | ||||
|         if self.tp_q: | ||||
|             self.start_threads(4) | ||||
|  | ||||
| @@ -135,7 +183,7 @@ class HttpSrv(object): | ||||
|             if self.args.log_thrs: | ||||
|                 start_log_thrs(self.log, self.args.log_thrs, nid) | ||||
|  | ||||
|         self.th_cfg: dict[str, Any] = {} | ||||
|         self.th_cfg: dict[str, set[str]] = {} | ||||
|         Daemon(self.post_init, "hsrv-init2") | ||||
|  | ||||
|     def post_init(self) -> None: | ||||
| @@ -145,12 +193,20 @@ class HttpSrv(object): | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|     def _build_statics(self) -> None: | ||||
|         for dp, _, df in os.walk(os.path.join(self.E.mod, "web")): | ||||
|             for fn in df: | ||||
|                 ap = absreal(os.path.join(dp, fn)) | ||||
|                 self.statics.add(ap) | ||||
|                 if ap.endswith(".gz"): | ||||
|                     self.statics.add(ap[:-3]) | ||||
|  | ||||
|     def set_netdevs(self, netdevs: dict[str, Netdev]) -> None: | ||||
|         ips = set() | ||||
|         for ip, _ in self.bound: | ||||
|             ips.add(ip) | ||||
|  | ||||
|         self.nm = NetMap(list(ips), netdevs) | ||||
|         self.nm = NetMap(list(ips), list(netdevs)) | ||||
|  | ||||
|     def start_threads(self, n: int) -> None: | ||||
|         self.tp_nthr += n | ||||
| @@ -172,7 +228,7 @@ class HttpSrv(object): | ||||
|     def periodic(self) -> None: | ||||
|         while True: | ||||
|             time.sleep(2 if self.tp_ncli or self.ncli else 10) | ||||
|             with self.mutex: | ||||
|             with self.u2mutex, self.mutex: | ||||
|                 self.u2fh.clean() | ||||
|                 if self.tp_q: | ||||
|                     self.tp_ncli = max(self.ncli, self.tp_ncli - 2) | ||||
| @@ -210,10 +266,7 @@ class HttpSrv(object): | ||||
|         msg = "subscribed @ {}:{}  f{} p{}".format(hip, port, fno, os.getpid()) | ||||
|         self.log(self.name, msg) | ||||
|  | ||||
|         def fun() -> None: | ||||
|             self.broker.say("cb_httpsrv_up") | ||||
|  | ||||
|         threading.Thread(target=fun, name="sig-hsrv-up1").start() | ||||
|         Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",)) | ||||
|  | ||||
|         while not self.stopping: | ||||
|             if self.args.log_conn: | ||||
| @@ -318,7 +371,7 @@ class HttpSrv(object): | ||||
|             if not self.t_periodic: | ||||
|                 name = "hsrv-pt" | ||||
|                 if self.nid: | ||||
|                     name += "-{}".format(self.nid) | ||||
|                     name += "-%d" % (self.nid,) | ||||
|  | ||||
|                 self.t_periodic = Daemon(self.periodic, name) | ||||
|  | ||||
| @@ -337,7 +390,7 @@ class HttpSrv(object): | ||||
|  | ||||
|         Daemon( | ||||
|             self.thr_client, | ||||
|             "httpconn-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]), | ||||
|             "httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1]), | ||||
|             (sck, addr), | ||||
|         ) | ||||
|  | ||||
| @@ -354,9 +407,7 @@ class HttpSrv(object): | ||||
|             try: | ||||
|                 sck, addr = task | ||||
|                 me = threading.current_thread() | ||||
|                 me.name = "httpconn-{}-{}".format( | ||||
|                     addr[0].split(".", 2)[-1][-6:], addr[1] | ||||
|                 ) | ||||
|                 me.name = "httpconn-%s-%d" % (addr[0].split(".", 2)[-1][-6:], addr[1]) | ||||
|                 self.thr_client(sck, addr) | ||||
|                 me.name = self.name + "-poolw" | ||||
|             except Exception as ex: | ||||
| @@ -436,6 +487,9 @@ class HttpSrv(object): | ||||
|                     self.clients.remove(cli) | ||||
|                     self.ncli -= 1 | ||||
|  | ||||
|                 if cli.u2idx: | ||||
|                     self.put_u2idx(str(addr), cli.u2idx) | ||||
|  | ||||
|     def cachebuster(self) -> str: | ||||
|         if time.time() - self.cb_ts < 1: | ||||
|             return self.cb_v | ||||
| @@ -457,3 +511,31 @@ class HttpSrv(object): | ||||
|             self.cb_v = v.decode("ascii")[-4:] | ||||
|             self.cb_ts = time.time() | ||||
|             return self.cb_v | ||||
|  | ||||
|     def get_u2idx(self, ident: str) -> Optional[U2idx]: | ||||
|         utab = self.u2idx_free | ||||
|         for _ in range(100):  # 5/0.05 = 5sec | ||||
|             with self.mutex: | ||||
|                 if utab: | ||||
|                     if ident in utab: | ||||
|                         return utab.pop(ident) | ||||
|  | ||||
|                     return utab.pop(list(utab.keys())[0]) | ||||
|  | ||||
|                 if self.u2idx_n < CORES: | ||||
|                     self.u2idx_n += 1 | ||||
|                     return U2idx(self) | ||||
|  | ||||
|             time.sleep(0.05) | ||||
|             # not using conditional waits, on a hunch that | ||||
|             # average performance will be faster like this | ||||
|             # since most servers won't be fully saturated | ||||
|  | ||||
|         return None | ||||
|  | ||||
|     def put_u2idx(self, ident: str, u2idx: U2idx) -> None: | ||||
|         with self.mutex: | ||||
|             while ident in self.u2idx_free: | ||||
|                 ident += "a" | ||||
|  | ||||
|             self.u2idx_free[ident] = u2idx | ||||
|   | ||||
| @@ -4,10 +4,11 @@ from __future__ import print_function, unicode_literals | ||||
| import argparse  # typechk | ||||
| import colorsys | ||||
| import hashlib | ||||
| import re | ||||
|  | ||||
| from .__init__ import PY2 | ||||
| from .th_srv import HAVE_PIL | ||||
| from .util import BytesIO | ||||
| from .th_srv import HAVE_PIL, HAVE_PILF | ||||
| from .util import BytesIO, html_escape  # type: ignore | ||||
|  | ||||
|  | ||||
| class Ico(object): | ||||
| @@ -17,54 +18,78 @@ class Ico(object): | ||||
|     def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]: | ||||
|         """placeholder to make thumbnails not break""" | ||||
|  | ||||
|         zb = hashlib.sha1(ext.encode("utf-8")).digest()[2:4] | ||||
|         bext = ext.encode("ascii", "replace") | ||||
|         ext = bext.decode("utf-8") | ||||
|         zb = hashlib.sha1(bext).digest()[2:4] | ||||
|         if PY2: | ||||
|             zb = [ord(x) for x in zb] | ||||
|             zb = [ord(x) for x in zb]  # type: ignore | ||||
|  | ||||
|         c1 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 0.3) | ||||
|         c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 1, 1) | ||||
|         c2 = colorsys.hsv_to_rgb(zb[0] / 256.0, 0.8 if HAVE_PILF else 1, 1) | ||||
|         ci = [int(x * 255) for x in list(c1) + list(c2)] | ||||
|         c = "".join(["{:02x}".format(x) for x in ci]) | ||||
|         c = "".join(["%02x" % (x,) for x in ci]) | ||||
|  | ||||
|         w = 100 | ||||
|         h = 30 | ||||
|         if not self.args.th_no_crop and as_thumb: | ||||
|         if as_thumb: | ||||
|             sw, sh = self.args.th_size.split("x") | ||||
|             h = int(100 / (float(sw) / float(sh))) | ||||
|             w = 100 | ||||
|             h = int(100.0 / (float(sw) / float(sh))) | ||||
|  | ||||
|         if chrome and as_thumb: | ||||
|         if chrome: | ||||
|             # cannot handle more than ~2000 unique SVGs | ||||
|             if HAVE_PILF: | ||||
|                 # pillow 10.1 made this the default font; | ||||
|                 # svg: 3.7s, this: 36s | ||||
|                 try: | ||||
|                     from PIL import Image, ImageDraw | ||||
|  | ||||
|                     # [.lt] are hard to see lowercase / unspaced | ||||
|                     ext2 = re.sub("(.)", "\\1 ", ext).upper() | ||||
|  | ||||
|                     h = int(128.0 * h / w) | ||||
|                     w = 128 | ||||
|                     img = Image.new("RGB", (w, h), "#" + c[:6]) | ||||
|                     pb = ImageDraw.Draw(img) | ||||
|                     _, _, tw, th = pb.textbbox((0, 0), ext2, font_size=16) | ||||
|                     xy = (int((w - tw) / 2), int((h - th) / 2)) | ||||
|                     pb.text(xy, ext2, fill="#" + c[6:], font_size=16) | ||||
|  | ||||
|                     img = img.resize((w * 2, h * 2), Image.NEAREST) | ||||
|  | ||||
|                     buf = BytesIO() | ||||
|                     img.save(buf, format="PNG", compress_level=1) | ||||
|                     return "image/png", buf.getvalue() | ||||
|  | ||||
|                 except: | ||||
|                     pass | ||||
|  | ||||
|             if HAVE_PIL: | ||||
|                 # svg: 3s, cache: 6s, this: 8s | ||||
|                 from PIL import Image, ImageDraw | ||||
|  | ||||
|                 h = int(64 * h / w) | ||||
|                 h = int(64.0 * h / w) | ||||
|                 w = 64 | ||||
|                 img = Image.new("RGB", (w, h), "#" + c[:6]) | ||||
|                 pb = ImageDraw.Draw(img) | ||||
|                 tw, th = pb.textsize(ext) | ||||
|                 pb.text(((w - tw) // 2, (h - th) // 2), ext, fill="#" + c[6:]) | ||||
|                 try: | ||||
|                     _, _, tw, th = pb.textbbox((0, 0), ext) | ||||
|                 except: | ||||
|                     tw, th = pb.textsize(ext) | ||||
|  | ||||
|                 tw += len(ext) | ||||
|                 cw = tw // len(ext) | ||||
|                 x = ((w - tw) // 2) - (cw * 2) // 3 | ||||
|                 fill = "#" + c[6:] | ||||
|                 for ch in ext: | ||||
|                     pb.text((x, (h - th) // 2), " %s " % (ch,), fill=fill) | ||||
|                     x += cw | ||||
|  | ||||
|                 img = img.resize((w * 3, h * 3), Image.NEAREST) | ||||
|  | ||||
|                 buf = BytesIO() | ||||
|                 img.save(buf, format="PNG", compress_level=1) | ||||
|                 return "image/png", buf.getvalue() | ||||
|  | ||||
|             elif False: | ||||
|                 # 48s, too slow | ||||
|                 import pyvips | ||||
|  | ||||
|                 h = int(192 * h / w) | ||||
|                 w = 192 | ||||
|                 img = pyvips.Image.text( | ||||
|                     ext, width=w, height=h, dpi=192, align=pyvips.Align.CENTRE | ||||
|                 ) | ||||
|                 img = img.ifthenelse(ci[3:], ci[:3], blend=True) | ||||
|                 # i = i.resize(3, kernel=pyvips.Kernel.NEAREST) | ||||
|                 buf = img.write_to_buffer(".png[compression=1]") | ||||
|                 return "image/png", buf | ||||
|  | ||||
|         svg = """\ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg version="1.1" viewBox="0 0 100 {}" xmlns="http://www.w3.org/2000/svg"><g> | ||||
| @@ -73,6 +98,6 @@ class Ico(object): | ||||
|   fill="#{}" font-family="monospace" font-size="14px" style="letter-spacing:.5px">{}</text> | ||||
| </g></svg> | ||||
| """ | ||||
|         svg = svg.format(h, c[:6], c[6:], ext) | ||||
|         svg = svg.format(h, c[:6], c[6:], html_escape(ext, True)) | ||||
|  | ||||
|         return "image/svg+xml", svg.encode("utf-8") | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import errno | ||||
| import random | ||||
| import select | ||||
| import socket | ||||
| @@ -11,6 +12,7 @@ from ipaddress import IPv4Network, IPv6Network | ||||
| from .__init__ import TYPE_CHECKING | ||||
| from .__init__ import unicode as U | ||||
| from .multicast import MC_Sck, MCast | ||||
| from .stolen.dnslib import AAAA | ||||
| from .stolen.dnslib import CLASS as DC | ||||
| from .stolen.dnslib import ( | ||||
|     NSEC, | ||||
| @@ -20,7 +22,6 @@ from .stolen.dnslib import ( | ||||
|     SRV, | ||||
|     TXT, | ||||
|     A, | ||||
|     AAAA, | ||||
|     DNSHeader, | ||||
|     DNSQuestion, | ||||
|     DNSRecord, | ||||
| @@ -277,12 +278,26 @@ class MDNS(MCast): | ||||
|         zf = time.time() + 2 | ||||
|         self.probing = zf  # cant unicast so give everyone an extra sec | ||||
|         self.unsolicited = [zf, zf + 1, zf + 3, zf + 7]  # rfc-8.3 | ||||
|  | ||||
|         try: | ||||
|             self.run2() | ||||
|         except OSError as ex: | ||||
|             if ex.errno != errno.EBADF: | ||||
|                 raise | ||||
|  | ||||
|             self.log("stopping due to {}".format(ex), "90") | ||||
|  | ||||
|         self.log("stopped", 2) | ||||
|  | ||||
|     def run2(self) -> None: | ||||
|         last_hop = time.time() | ||||
|         ihop = self.args.mc_hop | ||||
|         while self.running: | ||||
|             timeout = ( | ||||
|                 0.02 + random.random() * 0.07 | ||||
|                 if self.probing or self.q or self.defend or self.unsolicited | ||||
|                 if self.probing or self.q or self.defend | ||||
|                 else max(0.05, self.unsolicited[0] - time.time()) | ||||
|                 if self.unsolicited | ||||
|                 else (last_hop + ihop if ihop else 180) | ||||
|             ) | ||||
|             rdy = select.select(self.srv, [], [], timeout) | ||||
| @@ -314,8 +329,6 @@ class MDNS(MCast): | ||||
|                 self.log(t.format(self.hn[:-1]), 2) | ||||
|                 self.probing = 0 | ||||
|  | ||||
|         self.log("stopped", 2) | ||||
|  | ||||
|     def stop(self, panic=False) -> None: | ||||
|         self.running = False | ||||
|         for srv in self.srv.values(): | ||||
| @@ -502,6 +515,10 @@ class MDNS(MCast): | ||||
|             for srv in self.srv.values(): | ||||
|                 tx.add(srv) | ||||
|  | ||||
|             if not self.unsolicited and self.args.zm_spam: | ||||
|                 zf = time.time() + self.args.zm_spam + random.random() * 0.07 | ||||
|                 self.unsolicited.append(zf) | ||||
|  | ||||
|         for srv, deadline in list(self.defend.items()): | ||||
|             if now < deadline: | ||||
|                 continue | ||||
|   | ||||
							
								
								
									
										236
									
								
								copyparty/metrics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								copyparty/metrics.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import json | ||||
| import time | ||||
|  | ||||
| from .__init__ import TYPE_CHECKING | ||||
| from .util import Pebkac, get_df, unhumanize | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .httpcli import HttpCli | ||||
|     from .httpsrv import HttpSrv | ||||
|  | ||||
|  | ||||
| class Metrics(object): | ||||
|     def __init__(self, hsrv: "HttpSrv") -> None: | ||||
|         self.hsrv = hsrv | ||||
|  | ||||
|     def tx(self, cli: "HttpCli") -> bool: | ||||
|         if not cli.avol: | ||||
|             raise Pebkac(403, "not allowed for user " + cli.uname) | ||||
|  | ||||
|         args = cli.args | ||||
|         if not args.stats: | ||||
|             raise Pebkac(403, "the stats feature is not enabled in server config") | ||||
|  | ||||
|         conn = cli.conn | ||||
|         vfs = conn.asrv.vfs | ||||
|         allvols = list(sorted(vfs.all_vols.items())) | ||||
|  | ||||
|         idx = conn.get_u2idx() | ||||
|         if not idx or not hasattr(idx, "p_end"): | ||||
|             idx = None | ||||
|  | ||||
|         ret: list[str] = [] | ||||
|  | ||||
|         def addc(k: str, v: str, desc: str) -> None: | ||||
|             zs = "# TYPE %s counter\n# HELP %s %s\n%s_created %s\n%s_total %s" | ||||
|             ret.append(zs % (k, k, desc, k, int(self.hsrv.t0), k, v)) | ||||
|  | ||||
|         def adduc(k: str, unit: str, v: str, desc: str) -> None: | ||||
|             k += "_" + unit | ||||
|             zs = "# TYPE %s counter\n# UNIT %s %s\n# HELP %s %s\n%s_created %s\n%s_total %s" | ||||
|             ret.append(zs % (k, k, unit, k, desc, k, int(self.hsrv.t0), k, v)) | ||||
|  | ||||
|         def addg(k: str, v: str, desc: str) -> None: | ||||
|             zs = "# TYPE %s gauge\n# HELP %s %s\n%s %s" | ||||
|             ret.append(zs % (k, k, desc, k, v)) | ||||
|  | ||||
|         def addug(k: str, unit: str, v: str, desc: str) -> None: | ||||
|             k += "_" + unit | ||||
|             zs = "# TYPE %s gauge\n# UNIT %s %s\n# HELP %s %s\n%s %s" | ||||
|             ret.append(zs % (k, k, unit, k, desc, k, v)) | ||||
|  | ||||
|         def addh(k: str, typ: str, desc: str) -> None: | ||||
|             zs = "# TYPE %s %s\n# HELP %s %s" | ||||
|             ret.append(zs % (k, typ, k, desc)) | ||||
|  | ||||
|         def addbh(k: str, desc: str) -> None: | ||||
|             zs = "# TYPE %s gauge\n# UNIT %s bytes\n# HELP %s %s" | ||||
|             ret.append(zs % (k, k, k, desc)) | ||||
|  | ||||
|         def addv(k: str, v: str) -> None: | ||||
|             ret.append("%s %s" % (k, v)) | ||||
|  | ||||
|         t = "time since last copyparty restart" | ||||
|         v = "{:.3f}".format(time.time() - self.hsrv.t0) | ||||
|         addug("cpp_uptime", "seconds", v, t) | ||||
|  | ||||
|         # timestamps are gauges because initial value is not zero | ||||
|         t = "unixtime of last copyparty restart" | ||||
|         v = "{:.3f}".format(self.hsrv.t0) | ||||
|         addug("cpp_boot_unixtime", "seconds", v, t) | ||||
|  | ||||
|         t = "number of open http(s) client connections" | ||||
|         addg("cpp_http_conns", str(self.hsrv.ncli), t) | ||||
|  | ||||
|         t = "number of http(s) requests since last restart" | ||||
|         addc("cpp_http_reqs", str(self.hsrv.nreq), t) | ||||
|  | ||||
|         t = "number of 403/422/malicious reqs since restart" | ||||
|         addc("cpp_sus_reqs", str(self.hsrv.nsus), t) | ||||
|  | ||||
|         v = str(len(conn.bans or [])) | ||||
|         addg("cpp_active_bans", v, "number of currently banned IPs") | ||||
|  | ||||
|         t = "number of IPs banned since last restart" | ||||
|         addg("cpp_total_bans", str(self.hsrv.nban), t) | ||||
|  | ||||
|         if not args.nos_vst: | ||||
|             x = self.hsrv.broker.ask("up2k.get_state") | ||||
|             vs = json.loads(x.get()) | ||||
|  | ||||
|             nvidle = 0 | ||||
|             nvbusy = 0 | ||||
|             nvoffline = 0 | ||||
|             for v in vs["volstate"].values(): | ||||
|                 if v == "online, idle": | ||||
|                     nvidle += 1 | ||||
|                 elif "OFFLINE" in v: | ||||
|                     nvoffline += 1 | ||||
|                 else: | ||||
|                     nvbusy += 1 | ||||
|  | ||||
|             addg("cpp_idle_vols", str(nvidle), "number of idle/ready volumes") | ||||
|             addg("cpp_busy_vols", str(nvbusy), "number of busy/indexing volumes") | ||||
|             addg("cpp_offline_vols", str(nvoffline), "number of offline volumes") | ||||
|  | ||||
|             t = "time since last database activity (upload/rename/delete)" | ||||
|             addug("cpp_db_idle", "seconds", str(vs["dbwt"]), t) | ||||
|  | ||||
|             t = "unixtime of last database activity (upload/rename/delete)" | ||||
|             addug("cpp_db_act", "seconds", str(vs["dbwu"]), t) | ||||
|  | ||||
|             t = "number of files queued for hashing/indexing" | ||||
|             addg("cpp_hashing_files", str(vs["hashq"]), t) | ||||
|  | ||||
|             t = "number of files queued for metadata scanning" | ||||
|             addg("cpp_tagq_files", str(vs["tagq"]), t) | ||||
|  | ||||
|             try: | ||||
|                 t = "number of files queued for plugin-based analysis" | ||||
|                 addg("cpp_mtpq_files", str(int(vs["mtpq"])), t) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|         if not args.nos_hdd: | ||||
|             addbh("cpp_disk_size_bytes", "total HDD size of volume") | ||||
|             addbh("cpp_disk_free_bytes", "free HDD space in volume") | ||||
|             for vpath, vol in allvols: | ||||
|                 free, total = get_df(vol.realpath) | ||||
|                 if free is None or total is None: | ||||
|                     continue | ||||
|  | ||||
|                 addv('cpp_disk_size_bytes{vol="/%s"}' % (vpath), str(total)) | ||||
|                 addv('cpp_disk_free_bytes{vol="/%s"}' % (vpath), str(free)) | ||||
|  | ||||
|         if idx and not args.nos_vol: | ||||
|             addbh("cpp_vol_bytes", "num bytes of data in volume") | ||||
|             addh("cpp_vol_files", "gauge", "num files in volume") | ||||
|             addbh("cpp_vol_free_bytes", "free space (vmaxb) in volume") | ||||
|             addh("cpp_vol_free_files", "gauge", "free space (vmaxn) in volume") | ||||
|             tnbytes = 0 | ||||
|             tnfiles = 0 | ||||
|  | ||||
|             volsizes = [] | ||||
|             try: | ||||
|                 ptops = [x.realpath for _, x in allvols] | ||||
|                 x = self.hsrv.broker.ask("up2k.get_volsizes", ptops) | ||||
|                 volsizes = x.get() | ||||
|             except Exception as ex: | ||||
|                 cli.log("tx_stats get_volsizes: {!r}".format(ex), 3) | ||||
|  | ||||
|             for (vpath, vol), (nbytes, nfiles) in zip(allvols, volsizes): | ||||
|                 tnbytes += nbytes | ||||
|                 tnfiles += nfiles | ||||
|                 addv('cpp_vol_bytes{vol="/%s"}' % (vpath), str(nbytes)) | ||||
|                 addv('cpp_vol_files{vol="/%s"}' % (vpath), str(nfiles)) | ||||
|  | ||||
|                 if vol.flags.get("vmaxb") or vol.flags.get("vmaxn"): | ||||
|  | ||||
|                     zi = unhumanize(vol.flags.get("vmaxb") or "0") | ||||
|                     if zi: | ||||
|                         v = str(zi - nbytes) | ||||
|                         addv('cpp_vol_free_bytes{vol="/%s"}' % (vpath), v) | ||||
|  | ||||
|                     zi = unhumanize(vol.flags.get("vmaxn") or "0") | ||||
|                     if zi: | ||||
|                         v = str(zi - nfiles) | ||||
|                         addv('cpp_vol_free_files{vol="/%s"}' % (vpath), v) | ||||
|  | ||||
|             if volsizes: | ||||
|                 addv('cpp_vol_bytes{vol="total"}', str(tnbytes)) | ||||
|                 addv('cpp_vol_files{vol="total"}', str(tnfiles)) | ||||
|  | ||||
|         if idx and not args.nos_dup: | ||||
|             addbh("cpp_dupe_bytes", "num dupe bytes in volume") | ||||
|             addh("cpp_dupe_files", "gauge", "num dupe files in volume") | ||||
|             tnbytes = 0 | ||||
|             tnfiles = 0 | ||||
|             for vpath, vol in allvols: | ||||
|                 cur = idx.get_cur(vol) | ||||
|                 if not cur: | ||||
|                     continue | ||||
|  | ||||
|                 nbytes = 0 | ||||
|                 nfiles = 0 | ||||
|                 q = "select sz, count(*)-1 c from up group by w having c" | ||||
|                 for sz, c in cur.execute(q): | ||||
|                     nbytes += sz * c | ||||
|                     nfiles += c | ||||
|  | ||||
|                 tnbytes += nbytes | ||||
|                 tnfiles += nfiles | ||||
|                 addv('cpp_dupe_bytes{vol="/%s"}' % (vpath), str(nbytes)) | ||||
|                 addv('cpp_dupe_files{vol="/%s"}' % (vpath), str(nfiles)) | ||||
|  | ||||
|             addv('cpp_dupe_bytes{vol="total"}', str(tnbytes)) | ||||
|             addv('cpp_dupe_files{vol="total"}', str(tnfiles)) | ||||
|  | ||||
|         if not args.nos_unf: | ||||
|             addbh("cpp_unf_bytes", "incoming/unfinished uploads (num bytes)") | ||||
|             addh("cpp_unf_files", "gauge", "incoming/unfinished uploads (num files)") | ||||
|             tnbytes = 0 | ||||
|             tnfiles = 0 | ||||
|             try: | ||||
|                 x = self.hsrv.broker.ask("up2k.get_unfinished") | ||||
|                 xs = x.get() | ||||
|                 if not xs: | ||||
|                     raise Exception("up2k mutex acquisition timed out") | ||||
|  | ||||
|                 xj = json.loads(xs) | ||||
|                 for ptop, (nbytes, nfiles) in xj.items(): | ||||
|                     tnbytes += nbytes | ||||
|                     tnfiles += nfiles | ||||
|                     vol = next((x[1] for x in allvols if x[1].realpath == ptop), None) | ||||
|                     if not vol: | ||||
|                         t = "tx_stats get_unfinished: could not map {}" | ||||
|                         cli.log(t.format(ptop), 3) | ||||
|                         continue | ||||
|  | ||||
|                     addv('cpp_unf_bytes{vol="/%s"}' % (vol.vpath), str(nbytes)) | ||||
|                     addv('cpp_unf_files{vol="/%s"}' % (vol.vpath), str(nfiles)) | ||||
|  | ||||
|                 addv('cpp_unf_bytes{vol="total"}', str(tnbytes)) | ||||
|                 addv('cpp_unf_files{vol="total"}', str(tnfiles)) | ||||
|  | ||||
|             except Exception as ex: | ||||
|                 cli.log("tx_stats get_unfinished: {!r}".format(ex), 3) | ||||
|  | ||||
|         ret.append("# EOF") | ||||
|  | ||||
|         mime = "application/openmetrics-text; version=1.0.0; charset=utf-8" | ||||
|         mime = cli.uparam.get("mime") or mime | ||||
|         cli.reply("\n".join(ret).encode("utf-8"), mime=mime) | ||||
|         return True | ||||
| @@ -7,18 +7,35 @@ import os | ||||
| import shutil | ||||
| import subprocess as sp | ||||
| import sys | ||||
| import tempfile | ||||
|  | ||||
| from .__init__ import PY2, WINDOWS, E, unicode | ||||
| from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode | ||||
| from .authsrv import VFS | ||||
| from .bos import bos | ||||
| from .util import REKOBO_LKEY, fsenc, min_ex, retchk, runcmd, uncyg | ||||
| from .util import ( | ||||
|     FFMPEG_URL, | ||||
|     REKOBO_LKEY, | ||||
|     VF_CAREFUL, | ||||
|     fsenc, | ||||
|     min_ex, | ||||
|     pybin, | ||||
|     retchk, | ||||
|     runcmd, | ||||
|     sfsenc, | ||||
|     uncyg, | ||||
|     wunlink, | ||||
| ) | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Any, Union | ||||
|     from typing import Any, Optional, Union | ||||
|  | ||||
|     from .util import RootLogger | ||||
|     from .util import NamedLogger, RootLogger | ||||
|  | ||||
|  | ||||
| def have_ff(scmd: str) -> bool: | ||||
|     if ANYWIN: | ||||
|         scmd += ".exe" | ||||
|  | ||||
|     if PY2: | ||||
|         print("# checking {}".format(scmd)) | ||||
|         acmd = (scmd + " -version").encode("ascii").split(b" ") | ||||
| @@ -94,6 +111,51 @@ class MParser(object): | ||||
|             raise Exception() | ||||
|  | ||||
|  | ||||
| def au_unpk(log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None) -> str: | ||||
|     ret = "" | ||||
|     try: | ||||
|         ext = abspath.split(".")[-1].lower() | ||||
|         au, pk = fmt_map[ext].split(".") | ||||
|  | ||||
|         fd, ret = tempfile.mkstemp("." + au) | ||||
|  | ||||
|         if pk == "gz": | ||||
|             import gzip | ||||
|  | ||||
|             fi = gzip.GzipFile(abspath, mode="rb") | ||||
|  | ||||
|         elif pk == "xz": | ||||
|             import lzma | ||||
|  | ||||
|             fi = lzma.open(abspath, "rb") | ||||
|  | ||||
|         elif pk == "zip": | ||||
|             import zipfile | ||||
|  | ||||
|             zf = zipfile.ZipFile(abspath, "r") | ||||
|             zil = zf.infolist() | ||||
|             zil = [x for x in zil if x.filename.lower().split(".")[-1] == au] | ||||
|             fi = zf.open(zil[0]) | ||||
|  | ||||
|         with os.fdopen(fd, "wb") as fo: | ||||
|             while True: | ||||
|                 buf = fi.read(32768) | ||||
|                 if not buf: | ||||
|                     break | ||||
|  | ||||
|                 fo.write(buf) | ||||
|  | ||||
|         return ret | ||||
|  | ||||
|     except Exception as ex: | ||||
|         if ret: | ||||
|             t = "failed to decompress audio file [%s]: %r" | ||||
|             log(t % (abspath, ex)) | ||||
|             wunlink(log, ret, vn.flags if vn else VF_CAREFUL) | ||||
|  | ||||
|         return abspath | ||||
|  | ||||
|  | ||||
| def ffprobe( | ||||
|     abspath: str, timeout: int = 60 | ||||
| ) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: | ||||
| @@ -105,7 +167,7 @@ def ffprobe( | ||||
|         b"--", | ||||
|         fsenc(abspath), | ||||
|     ] | ||||
|     rc, so, se = runcmd(cmd, timeout=timeout) | ||||
|     rc, so, se = runcmd(cmd, timeout=timeout, nice=True, oom=200) | ||||
|     retchk(rc, cmd, se) | ||||
|     return parse_ffprobe(so) | ||||
|  | ||||
| @@ -227,7 +289,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ | ||||
|         if "/" in fps: | ||||
|             fa, fb = fps.split("/") | ||||
|             try: | ||||
|                 fps = int(fa) * 1.0 / int(fb) | ||||
|                 fps = float(fa) / float(fb) | ||||
|             except: | ||||
|                 fps = 9001 | ||||
|  | ||||
| @@ -248,7 +310,8 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ | ||||
|     if ".resw" in ret and ".resh" in ret: | ||||
|         ret["res"] = "{}x{}".format(ret[".resw"], ret[".resh"]) | ||||
|  | ||||
|     zd = {k: (0, v) for k, v in ret.items()} | ||||
|     zero = int("0") | ||||
|     zd = {k: (zero, v) for k, v in ret.items()} | ||||
|  | ||||
|     return zd, md | ||||
|  | ||||
| @@ -259,13 +322,15 @@ class MTag(object): | ||||
|         self.args = args | ||||
|         self.usable = True | ||||
|         self.prefer_mt = not args.no_mtag_ff | ||||
|         self.backend = "ffprobe" if args.no_mutagen else "mutagen" | ||||
|         self.backend = ( | ||||
|             "ffprobe" if args.no_mutagen or (HAVE_FFPROBE and EXE) else "mutagen" | ||||
|         ) | ||||
|         self.can_ffprobe = HAVE_FFPROBE and not args.no_mtag_ff | ||||
|         mappings = args.mtm | ||||
|         or_ffprobe = " or FFprobe" | ||||
|  | ||||
|         if self.backend == "mutagen": | ||||
|             self.get = self.get_mutagen | ||||
|             self._get = self.get_mutagen | ||||
|             try: | ||||
|                 from mutagen import version  # noqa: F401 | ||||
|             except: | ||||
| @@ -274,7 +339,7 @@ class MTag(object): | ||||
|  | ||||
|         if self.backend == "ffprobe": | ||||
|             self.usable = self.can_ffprobe | ||||
|             self.get = self.get_ffprobe | ||||
|             self._get = self.get_ffprobe | ||||
|             self.prefer_mt = True | ||||
|  | ||||
|             if not HAVE_FFPROBE: | ||||
| @@ -285,9 +350,14 @@ class MTag(object): | ||||
|                 self.log(msg, c=3) | ||||
|  | ||||
|         if not self.usable: | ||||
|             if EXE: | ||||
|                 t = "copyparty.exe cannot use mutagen; need ffprobe.exe to read media tags: " | ||||
|                 self.log(t + FFMPEG_URL) | ||||
|                 return | ||||
|  | ||||
|             msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n" | ||||
|             pybin = os.path.basename(sys.executable) | ||||
|             self.log(msg.format(or_ffprobe, " " * 37, pybin), c=1) | ||||
|             pyname = os.path.basename(pybin) | ||||
|             self.log(msg.format(or_ffprobe, " " * 37, pyname), c=1) | ||||
|             return | ||||
|  | ||||
|         # https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html | ||||
| @@ -439,6 +509,17 @@ class MTag(object): | ||||
|  | ||||
|         return r1 | ||||
|  | ||||
|     def get(self, abspath: str) -> dict[str, Union[str, float]]: | ||||
|         ext = abspath.split(".")[-1].lower() | ||||
|         if ext not in self.args.au_unpk: | ||||
|             return self._get(abspath) | ||||
|  | ||||
|         ap = au_unpk(self.log, self.args.au_unpk, abspath) | ||||
|         ret = self._get(ap) | ||||
|         if ap != abspath: | ||||
|             wunlink(self.log, ap, VF_CAREFUL) | ||||
|         return ret | ||||
|  | ||||
|     def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]: | ||||
|         ret: dict[str, tuple[int, Any]] = {} | ||||
|  | ||||
| @@ -456,7 +537,10 @@ class MTag(object): | ||||
|                     self.log("mutagen: {}\033[0m".format(" ".join(zl)), "90") | ||||
|             if not md.info.length and not md.info.codec: | ||||
|                 raise Exception() | ||||
|         except: | ||||
|         except Exception as ex: | ||||
|             if self.args.mtag_v: | ||||
|                 self.log("mutagen-err [{}] @ [{}]".format(ex, abspath), "90") | ||||
|  | ||||
|             return self.get_ffprobe(abspath) if self.can_ffprobe else {} | ||||
|  | ||||
|         sz = bos.path.getsize(abspath) | ||||
| @@ -519,23 +603,33 @@ class MTag(object): | ||||
|  | ||||
|         env = os.environ.copy() | ||||
|         try: | ||||
|             if EXE: | ||||
|                 raise Exception() | ||||
|  | ||||
|             pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) | ||||
|             zsl = [str(pypath)] + [str(x) for x in sys.path if x] | ||||
|             pypath = str(os.pathsep.join(zsl)) | ||||
|             env["PYTHONPATH"] = pypath | ||||
|         except: | ||||
|             if not E.ox: | ||||
|                 raise | ||||
|             raise  # might be expected outside cpython | ||||
|  | ||||
|         ext = abspath.split(".")[-1].lower() | ||||
|         if ext in self.args.au_unpk: | ||||
|             ap = au_unpk(self.log, self.args.au_unpk, abspath) | ||||
|         else: | ||||
|             ap = abspath | ||||
|  | ||||
|         ret: dict[str, Any] = {} | ||||
|         for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])): | ||||
|             try: | ||||
|                 cmd = [parser.bin, abspath] | ||||
|                 cmd = [parser.bin, ap] | ||||
|                 if parser.bin.endswith(".py"): | ||||
|                     cmd = [sys.executable] + cmd | ||||
|                     cmd = [pybin] + cmd | ||||
|  | ||||
|                 args = { | ||||
|                     "env": env, | ||||
|                     "nice": True, | ||||
|                     "oom": 300, | ||||
|                     "timeout": parser.timeout, | ||||
|                     "kill": parser.kill, | ||||
|                     "capture": parser.capture, | ||||
| @@ -546,12 +640,7 @@ class MTag(object): | ||||
|                     zd.update(ret) | ||||
|                     args["sin"] = json.dumps(zd).encode("utf-8", "replace") | ||||
|  | ||||
|                 if WINDOWS: | ||||
|                     args["creationflags"] = 0x4000 | ||||
|                 else: | ||||
|                     cmd = ["nice"] + cmd | ||||
|  | ||||
|                 bcmd = [fsenc(x) for x in cmd] | ||||
|                 bcmd = [sfsenc(x) for x in cmd[:-1]] + [fsenc(cmd[-1])] | ||||
|                 rc, v, err = runcmd(bcmd, **args)  # type: ignore | ||||
|                 retchk(rc, bcmd, err, self.log, 5, self.args.mtag_v) | ||||
|                 v = v.strip() | ||||
| @@ -570,4 +659,7 @@ class MTag(object): | ||||
|                     t = "mtag error: tagname {}, parser {}, file {} => {}" | ||||
|                     self.log(t.format(tagname, parser.bin, abspath, min_ex())) | ||||
|  | ||||
|         if ap != abspath: | ||||
|             wunlink(self.log, ap, VF_CAREFUL) | ||||
|  | ||||
|         return ret | ||||
|   | ||||
| @@ -15,7 +15,7 @@ from ipaddress import ( | ||||
| ) | ||||
|  | ||||
| from .__init__ import MACOS, TYPE_CHECKING | ||||
| from .util import Netdev, find_prefix, min_ex, spack | ||||
| from .util import Daemon, Netdev, find_prefix, min_ex, spack | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .svchub import SvcHub | ||||
| @@ -110,7 +110,7 @@ class MCast(object): | ||||
|             ) | ||||
|  | ||||
|         ips = [x for x in ips if x not in ("::1", "127.0.0.1")] | ||||
|         ips = find_prefix(ips, netdevs) | ||||
|         ips = find_prefix(ips, list(netdevs)) | ||||
|  | ||||
|         on = self.on[:] | ||||
|         off = self.off[:] | ||||
| @@ -206,6 +206,7 @@ class MCast(object): | ||||
|             except: | ||||
|                 t = "announce failed on {} [{}]:\n{}" | ||||
|                 self.log(t.format(netdev, ip, min_ex()), 3) | ||||
|                 sck.close() | ||||
|  | ||||
|         if self.args.zm_msub: | ||||
|             for s1 in self.srv.values(): | ||||
| @@ -228,6 +229,7 @@ class MCast(object): | ||||
|         for srv in self.srv.values(): | ||||
|             assert srv.ip in self.sips | ||||
|  | ||||
|         Daemon(self.hopper, "mc-hop") | ||||
|         return bound | ||||
|  | ||||
|     def setup_socket(self, srv: MC_Sck) -> None: | ||||
| @@ -299,33 +301,57 @@ class MCast(object): | ||||
|                 t = "failed to set IPv4 TTL/LOOP; announcements may not survive multiple switches/routers" | ||||
|                 self.log(t, 3) | ||||
|  | ||||
|         self.hop(srv) | ||||
|         if self.hop(srv, False): | ||||
|             self.log("igmp was already joined?? chilling for a sec", 3) | ||||
|             time.sleep(1.2) | ||||
|  | ||||
|         self.hop(srv, True) | ||||
|         self.b4.sort(reverse=True) | ||||
|         self.b6.sort(reverse=True) | ||||
|  | ||||
|     def hop(self, srv: MC_Sck) -> None: | ||||
|     def hop(self, srv: MC_Sck, on: bool) -> bool: | ||||
|         """rejoin to keepalive on routers/switches without igmp-snooping""" | ||||
|         sck = srv.sck | ||||
|         req = srv.mreq | ||||
|         if ":" in srv.ip: | ||||
|             try: | ||||
|                 sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req) | ||||
|                 # linux does leaves/joins twice with 0.2~1.05s spacing | ||||
|                 time.sleep(1.2) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|             sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req) | ||||
|             if not on: | ||||
|                 try: | ||||
|                     sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_LEAVE_GROUP, req) | ||||
|                     return True | ||||
|                 except: | ||||
|                     return False | ||||
|             else: | ||||
|                 sck.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, req) | ||||
|         else: | ||||
|             try: | ||||
|                 sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req) | ||||
|                 time.sleep(1.2) | ||||
|             except: | ||||
|                 pass | ||||
|             if not on: | ||||
|                 try: | ||||
|                     sck.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, req) | ||||
|                     return True | ||||
|                 except: | ||||
|                     return False | ||||
|             else: | ||||
|                 # t = "joining {} from ip {} idx {} with mreq {}" | ||||
|                 # self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6) | ||||
|                 sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req) | ||||
|  | ||||
|             # t = "joining {} from ip {} idx {} with mreq {}" | ||||
|             # self.log(t.format(srv.grp, srv.ip, srv.idx, repr(srv.mreq)), 6) | ||||
|             sck.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, req) | ||||
|         return True | ||||
|  | ||||
|     def hopper(self): | ||||
|         while self.args.mc_hop and self.running: | ||||
|             time.sleep(self.args.mc_hop) | ||||
|             if not self.running: | ||||
|                 return | ||||
|  | ||||
|             for srv in self.srv.values(): | ||||
|                 self.hop(srv, False) | ||||
|  | ||||
|             # linux does leaves/joins twice with 0.2~1.05s spacing | ||||
|             time.sleep(1.2) | ||||
|             if not self.running: | ||||
|                 return | ||||
|  | ||||
|             for srv in self.srv.values(): | ||||
|                 self.hop(srv, True) | ||||
|  | ||||
|     def map_client(self, cip: str) -> Optional[MC_Sck]: | ||||
|         try: | ||||
|   | ||||
							
								
								
									
										149
									
								
								copyparty/pwhash.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								copyparty/pwhash.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import base64 | ||||
| import hashlib | ||||
| import sys | ||||
| import threading | ||||
|  | ||||
| from .__init__ import unicode | ||||
|  | ||||
|  | ||||
| class PWHash(object): | ||||
|     def __init__(self, args: argparse.Namespace): | ||||
|         self.args = args | ||||
|  | ||||
|         try: | ||||
|             alg, ac = args.ah_alg.split(",") | ||||
|         except: | ||||
|             alg = args.ah_alg | ||||
|             ac = {} | ||||
|  | ||||
|         if alg == "none": | ||||
|             alg = "" | ||||
|  | ||||
|         self.alg = alg | ||||
|         self.ac = ac | ||||
|         if not alg: | ||||
|             self.on = False | ||||
|             self.hash = unicode | ||||
|             return | ||||
|  | ||||
|         self.on = True | ||||
|         self.salt = args.ah_salt.encode("utf-8") | ||||
|         self.cache: dict[str, str] = {} | ||||
|         self.mutex = threading.Lock() | ||||
|         self.hash = self._cache_hash | ||||
|  | ||||
|         if alg == "sha2": | ||||
|             self._hash = self._gen_sha2 | ||||
|         elif alg == "scrypt": | ||||
|             self._hash = self._gen_scrypt | ||||
|         elif alg == "argon2": | ||||
|             self._hash = self._gen_argon2 | ||||
|         else: | ||||
|             t = "unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none" | ||||
|             raise Exception(t.format(alg)) | ||||
|  | ||||
|     def _cache_hash(self, plain: str) -> str: | ||||
|         with self.mutex: | ||||
|             try: | ||||
|                 return self.cache[plain] | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|             if not plain: | ||||
|                 return "" | ||||
|  | ||||
|             if len(plain) > 255: | ||||
|                 raise Exception("password too long") | ||||
|  | ||||
|             if len(self.cache) > 9000: | ||||
|                 self.cache = {} | ||||
|  | ||||
|             ret = self._hash(plain) | ||||
|             self.cache[plain] = ret | ||||
|             return ret | ||||
|  | ||||
|     def _gen_sha2(self, plain: str) -> str: | ||||
|         its = int(self.ac[0]) if self.ac else 424242 | ||||
|         bplain = plain.encode("utf-8") | ||||
|         ret = b"\n" | ||||
|         for _ in range(its): | ||||
|             ret = hashlib.sha512(self.salt + bplain + ret).digest() | ||||
|  | ||||
|         return "+" + base64.urlsafe_b64encode(ret[:24]).decode("utf-8") | ||||
|  | ||||
|     def _gen_scrypt(self, plain: str) -> str: | ||||
|         cost = 2 << 13 | ||||
|         its = 2 | ||||
|         blksz = 8 | ||||
|         para = 4 | ||||
|         try: | ||||
|             cost = 2 << int(self.ac[0]) | ||||
|             its = int(self.ac[1]) | ||||
|             blksz = int(self.ac[2]) | ||||
|             para = int(self.ac[3]) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         ret = plain.encode("utf-8") | ||||
|         for _ in range(its): | ||||
|             ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24) | ||||
|  | ||||
|         return "+" + base64.urlsafe_b64encode(ret).decode("utf-8") | ||||
|  | ||||
|     def _gen_argon2(self, plain: str) -> str: | ||||
|         from argon2.low_level import Type as ArgonType | ||||
|         from argon2.low_level import hash_secret | ||||
|  | ||||
|         time_cost = 3 | ||||
|         mem_cost = 256 | ||||
|         parallelism = 4 | ||||
|         version = 19 | ||||
|         try: | ||||
|             time_cost = int(self.ac[0]) | ||||
|             mem_cost = int(self.ac[1]) | ||||
|             parallelism = int(self.ac[2]) | ||||
|             version = int(self.ac[3]) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         bplain = plain.encode("utf-8") | ||||
|  | ||||
|         bret = hash_secret( | ||||
|             secret=bplain, | ||||
|             salt=self.salt, | ||||
|             time_cost=time_cost, | ||||
|             memory_cost=mem_cost * 1024, | ||||
|             parallelism=parallelism, | ||||
|             hash_len=24, | ||||
|             type=ArgonType.ID, | ||||
|             version=version, | ||||
|         ) | ||||
|         ret = bret.split(b"$")[-1].decode("utf-8") | ||||
|         return "+" + ret.replace("/", "_").replace("+", "-") | ||||
|  | ||||
|     def stdin(self) -> None: | ||||
|         while True: | ||||
|             ln = sys.stdin.readline().strip() | ||||
|             if not ln: | ||||
|                 break | ||||
|             print(self.hash(ln)) | ||||
|  | ||||
|     def cli(self) -> None: | ||||
|         import getpass | ||||
|  | ||||
|         while True: | ||||
|             try: | ||||
|                 p1 = getpass.getpass("password> ") | ||||
|                 p2 = getpass.getpass("again or just hit ENTER> ") | ||||
|             except EOFError: | ||||
|                 return | ||||
|  | ||||
|             if p2 and p1 != p2: | ||||
|                 print("\033[31minputs don't match; try again\033[0m", file=sys.stderr) | ||||
|                 continue | ||||
|             print(self.hash(p1)) | ||||
|             print() | ||||
							
								
								
									
										0
									
								
								copyparty/res/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								copyparty/res/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -9,13 +9,13 @@ import sys | ||||
| import time | ||||
| from types import SimpleNamespace | ||||
|  | ||||
| from .__init__ import ANYWIN, TYPE_CHECKING | ||||
| from .__init__ import ANYWIN, EXE, TYPE_CHECKING | ||||
| from .authsrv import LEELOO_DALLAS, VFS | ||||
| from .bos import bos | ||||
| from .util import Daemon, min_ex | ||||
| from .util import Daemon, min_ex, pybin, runhook | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Any | ||||
|     from typing import Any, Union | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .svchub import SvcHub | ||||
| @@ -32,6 +32,8 @@ class SMB(object): | ||||
|         self.asrv = hub.asrv | ||||
|         self.log = hub.log | ||||
|         self.files: dict[int, tuple[float, str]] = {} | ||||
|         self.noacc = self.args.smba | ||||
|         self.accs = not self.args.smba | ||||
|  | ||||
|         lg.setLevel(logging.DEBUG if self.args.smbvvv else logging.INFO) | ||||
|         for x in ["impacket", "impacket.smbserver"]: | ||||
| @@ -42,8 +44,12 @@ class SMB(object): | ||||
|             from impacket import smbserver | ||||
|             from impacket.ntlm import compute_lmhash, compute_nthash | ||||
|         except ImportError: | ||||
|             if EXE: | ||||
|                 print("copyparty.exe cannot do SMB") | ||||
|                 sys.exit(1) | ||||
|  | ||||
|             m = "\033[36m\n{}\033[31m\n\nERROR: need 'impacket'; please run this command:\033[33m\n {} -m pip install --user impacket\n\033[0m" | ||||
|             print(m.format(min_ex(), sys.executable)) | ||||
|             print(m.format(min_ex(), pybin)) | ||||
|             sys.exit(1) | ||||
|  | ||||
|         # patch vfs into smbserver.os | ||||
| @@ -90,6 +96,14 @@ class SMB(object): | ||||
|  | ||||
|         port = int(self.args.smb_port) | ||||
|         srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port) | ||||
|         try: | ||||
|             if self.accs: | ||||
|                 srv.setAuthCallback(self._auth_cb) | ||||
|         except: | ||||
|             self.accs = False | ||||
|             self.noacc = True | ||||
|             t = "impacket too old; access permissions will not work! all accounts are admin!" | ||||
|             self.log("smb", t, 1) | ||||
|  | ||||
|         ro = "no" if self.args.smbw else "yes"  # (does nothing) | ||||
|         srv.addShare("A", "/", readOnly=ro) | ||||
| @@ -109,27 +123,80 @@ class SMB(object): | ||||
|         self.stop = srv.stop | ||||
|         self.log("smb", "listening @ {}:{}".format(ip, port)) | ||||
|  | ||||
|     def start(self) -> None: | ||||
|         Daemon(self.srv.start) | ||||
|     def nlog(self, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         self.log("smb", msg, c) | ||||
|  | ||||
|     def _v2a(self, caller: str, vpath: str, *a: Any) -> tuple[VFS, str]: | ||||
|     def start(self) -> None: | ||||
|         Daemon(self.srv.start, "smbd") | ||||
|  | ||||
|     def _auth_cb(self, *a, **ka): | ||||
|         debug("auth-result: %s %s", a, ka) | ||||
|         conndata = ka["connData"] | ||||
|         auth_ok = conndata["Authenticated"] | ||||
|         uname = ka["user_name"] if auth_ok else "*" | ||||
|         uname = self.asrv.iacct.get(uname, uname) or "*" | ||||
|         oldname = conndata.get("partygoer", "*") or "*" | ||||
|         cli_ip = conndata["ClientIP"] | ||||
|         cli_hn = ka["host_name"] | ||||
|         if uname != "*": | ||||
|             conndata["partygoer"] = uname | ||||
|             info("client %s [%s] authed as %s", cli_ip, cli_hn, uname) | ||||
|         elif oldname != "*": | ||||
|             info("client %s [%s] keeping old auth as %s", cli_ip, cli_hn, oldname) | ||||
|         elif auth_ok: | ||||
|             info("client %s [%s] authed as [*] (anon)", cli_ip, cli_hn) | ||||
|         else: | ||||
|             info("client %s [%s] rejected", cli_ip, cli_hn) | ||||
|  | ||||
|     def _uname(self) -> str: | ||||
|         if self.noacc: | ||||
|             return LEELOO_DALLAS | ||||
|  | ||||
|         try: | ||||
|             # you found it! my single worst bit of code so far | ||||
|             # (if you can think of a better way to track users through impacket i'm all ears) | ||||
|             cf0 = inspect.currentframe().f_back.f_back | ||||
|             cf = cf0.f_back | ||||
|             for n in range(3): | ||||
|                 cl = cf.f_locals | ||||
|                 if "connData" in cl: | ||||
|                     return cl["connData"]["partygoer"] | ||||
|                 cf = cf.f_back | ||||
|             raise Exception() | ||||
|         except: | ||||
|             warning( | ||||
|                 "nyoron... %s <<-- %s <<-- %s <<-- %s", | ||||
|                 cf0.f_code.co_name, | ||||
|                 cf0.f_back.f_code.co_name, | ||||
|                 cf0.f_back.f_back.f_code.co_name, | ||||
|                 cf0.f_back.f_back.f_back.f_code.co_name, | ||||
|             ) | ||||
|             return "*" | ||||
|  | ||||
|     def _v2a( | ||||
|         self, caller: str, vpath: str, *a: Any, uname="", perms=None | ||||
|     ) -> tuple[VFS, str]: | ||||
|         vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|         # cf = inspect.currentframe().f_back | ||||
|         # c1 = cf.f_back.f_code.co_name | ||||
|         # c2 = cf.f_code.co_name | ||||
|         debug('%s("%s", %s)\033[K\033[0m', caller, vpath, str(a)) | ||||
|         if not uname: | ||||
|             uname = self._uname() | ||||
|         if not perms: | ||||
|             perms = [True, True] | ||||
|  | ||||
|         # TODO find a way to grab `identity` in smbComSessionSetupAndX and smb2SessionSetup | ||||
|         vfs, rem = self.asrv.vfs.get(vpath, LEELOO_DALLAS, True, True) | ||||
|         debug('%s("%s", %s) %s @%s\033[K\033[0m', caller, vpath, str(a), perms, uname) | ||||
|         vfs, rem = self.asrv.vfs.get(vpath, uname, *perms) | ||||
|         return vfs, vfs.canonical(rem) | ||||
|  | ||||
|     def _listdir(self, vpath: str, *a: Any, **ka: Any) -> list[str]: | ||||
|         vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|         # caller = inspect.currentframe().f_back.f_code.co_name | ||||
|         debug('listdir("%s", %s)\033[K\033[0m', vpath, str(a)) | ||||
|         vfs, rem = self.asrv.vfs.get(vpath, LEELOO_DALLAS, False, False) | ||||
|         uname = self._uname() | ||||
|         # debug('listdir("%s", %s) @%s\033[K\033[0m', vpath, str(a), uname) | ||||
|         vfs, rem = self.asrv.vfs.get(vpath, uname, False, False) | ||||
|         _, vfs_ls, vfs_virt = vfs.ls( | ||||
|             rem, LEELOO_DALLAS, not self.args.no_scandir, [[False, False]] | ||||
|             rem, uname, not self.args.no_scandir, [[False, False]] | ||||
|         ) | ||||
|         dirs = [x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] | ||||
|         fils = [x[0] for x in vfs_ls if x[0] not in dirs] | ||||
| @@ -142,8 +209,8 @@ class SMB(object): | ||||
|         sz = 112 * 2  # ['.', '..'] | ||||
|         for n, fn in enumerate(ls): | ||||
|             if sz >= 64000: | ||||
|                 t = "listing only %d of %d files (%d byte); see impacket#1433" | ||||
|                 warning(t, n, len(ls), sz) | ||||
|                 t = "listing only %d of %d files (%d byte) in /%s; see impacket#1433" | ||||
|                 warning(t, n, len(ls), sz, vpath) | ||||
|                 break | ||||
|  | ||||
|             nsz = len(fn.encode("utf-16", "replace")) | ||||
| @@ -164,9 +231,18 @@ class SMB(object): | ||||
|         if wr and not self.args.smbw: | ||||
|             yeet("blocked write (no --smbw): " + vpath) | ||||
|  | ||||
|         vfs, ap = self._v2a("open", vpath, *a) | ||||
|         if wr and not vfs.axs.uwrite: | ||||
|             yeet("blocked write (no-write-acc): " + vpath) | ||||
|         uname = self._uname() | ||||
|         vfs, ap = self._v2a("open", vpath, *a, uname=uname, perms=[True, wr]) | ||||
|         if wr: | ||||
|             if not vfs.axs.uwrite: | ||||
|                 t = "blocked write (no-write-acc %s): /%s @%s" | ||||
|                 yeet(t % (vfs.axs.uwrite, vpath, uname)) | ||||
|  | ||||
|             xbu = vfs.flags.get("xbu") | ||||
|             if xbu and not runhook( | ||||
|                 self.nlog, xbu, ap, vpath, "", "", 0, 0, "1.7.6.2", 0, "" | ||||
|             ): | ||||
|                 yeet("blocked by xbu server config: " + vpath) | ||||
|  | ||||
|         ret = bos.open(ap, flags, *a, mode=chmod, **ka) | ||||
|         if wr: | ||||
| @@ -190,15 +266,17 @@ class SMB(object): | ||||
|  | ||||
|         _, vp = self.files.pop(fd) | ||||
|         vp, fn = os.path.split(vp) | ||||
|         vfs, rem = self.hub.asrv.vfs.get(vp, LEELOO_DALLAS, False, True) | ||||
|         vfs, rem = self.hub.asrv.vfs.get(vp, self._uname(), False, True) | ||||
|         vfs, rem = vfs.get_dbv(rem) | ||||
|         self.hub.up2k.hash_file( | ||||
|             vfs.realpath, | ||||
|             vfs.vpath, | ||||
|             vfs.flags, | ||||
|             rem, | ||||
|             fn, | ||||
|             "1.7.6.2", | ||||
|             time.time(), | ||||
|             "", | ||||
|         ) | ||||
|  | ||||
|     def _rename(self, vp1: str, vp2: str) -> None: | ||||
| @@ -208,15 +286,18 @@ class SMB(object): | ||||
|         vp1 = vp1.lstrip("/") | ||||
|         vp2 = vp2.lstrip("/") | ||||
|  | ||||
|         vfs2, ap2 = self._v2a("rename", vp2, vp1) | ||||
|         uname = self._uname() | ||||
|         vfs2, ap2 = self._v2a("rename", vp2, vp1, uname=uname) | ||||
|         if not vfs2.axs.uwrite: | ||||
|             yeet("blocked rename (no-write-acc): " + vp2) | ||||
|             t = "blocked write (no-write-acc %s): /%s @%s" | ||||
|             yeet(t % (vfs2.axs.uwrite, vp2, uname)) | ||||
|  | ||||
|         vfs1, _ = self.asrv.vfs.get(vp1, LEELOO_DALLAS, True, True) | ||||
|         vfs1, _ = self.asrv.vfs.get(vp1, uname, True, True, True) | ||||
|         if not vfs1.axs.umove: | ||||
|             yeet("blocked rename (no-move-acc): " + vp1) | ||||
|             t = "blocked rename (no-move-acc %s): /%s @%s" | ||||
|             yeet(t % (vfs1.axs.umove, vp1, uname)) | ||||
|  | ||||
|         self.hub.up2k.handle_mv(LEELOO_DALLAS, vp1, vp2) | ||||
|         self.hub.up2k.handle_mv(uname, vp1, vp2) | ||||
|         try: | ||||
|             bos.makedirs(ap2) | ||||
|         except: | ||||
| @@ -226,52 +307,74 @@ class SMB(object): | ||||
|         if not self.args.smbw: | ||||
|             yeet("blocked mkdir (no --smbw): " + vpath) | ||||
|  | ||||
|         vfs, ap = self._v2a("mkdir", vpath) | ||||
|         uname = self._uname() | ||||
|         vfs, ap = self._v2a("mkdir", vpath, uname=uname) | ||||
|         if not vfs.axs.uwrite: | ||||
|             yeet("blocked mkdir (no-write-acc): " + vpath) | ||||
|             t = "blocked mkdir (no-write-acc %s): /%s @%s" | ||||
|             yeet(t % (vfs.axs.uwrite, vpath, uname)) | ||||
|  | ||||
|         return bos.mkdir(ap) | ||||
|  | ||||
|     def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result: | ||||
|         return bos.stat(self._v2a("stat", vpath, *a)[1], *a, **ka) | ||||
|         try: | ||||
|             ap = self._v2a("stat", vpath, *a, perms=[True, False])[1] | ||||
|             ret = bos.stat(ap, *a, **ka) | ||||
|             # debug(" `-stat:ok") | ||||
|             return ret | ||||
|         except: | ||||
|             # white lie: windows freaks out if we raise due to an offline volume | ||||
|             # debug(" `-stat:NOPE (faking a directory)") | ||||
|             ts = int(time.time()) | ||||
|             return os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, ts, ts, ts)) | ||||
|  | ||||
|     def _unlink(self, vpath: str) -> None: | ||||
|         if not self.args.smbw: | ||||
|             yeet("blocked delete (no --smbw): " + vpath) | ||||
|  | ||||
|         # return bos.unlink(self._v2a("stat", vpath, *a)[1]) | ||||
|         vfs, ap = self._v2a("delete", vpath) | ||||
|         uname = self._uname() | ||||
|         vfs, ap = self._v2a( | ||||
|             "delete", vpath, uname=uname, perms=[True, False, False, True] | ||||
|         ) | ||||
|         if not vfs.axs.udel: | ||||
|             yeet("blocked delete (no-del-acc): " + vpath) | ||||
|  | ||||
|         vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|         self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], []) | ||||
|         self.hub.up2k.handle_rm(uname, "1.7.6.2", [vpath], [], False, False) | ||||
|  | ||||
|     def _utime(self, vpath: str, times: tuple[float, float]) -> None: | ||||
|         if not self.args.smbw: | ||||
|             yeet("blocked utime (no --smbw): " + vpath) | ||||
|  | ||||
|         vfs, ap = self._v2a("utime", vpath) | ||||
|         uname = self._uname() | ||||
|         vfs, ap = self._v2a("utime", vpath, uname=uname) | ||||
|         if not vfs.axs.uwrite: | ||||
|             yeet("blocked utime (no-write-acc): " + vpath) | ||||
|             t = "blocked utime (no-write-acc %s): /%s @%s" | ||||
|             yeet(t % (vfs.axs.uwrite, vpath, uname)) | ||||
|  | ||||
|         return bos.utime(ap, times) | ||||
|  | ||||
|     def _p_exists(self, vpath: str) -> bool: | ||||
|         # ap = "?" | ||||
|         try: | ||||
|             bos.stat(self._v2a("p.exists", vpath)[1]) | ||||
|             ap = self._v2a("p.exists", vpath, perms=[True, False])[1] | ||||
|             bos.stat(ap) | ||||
|             # debug(" `-exists((%s)->(%s)):ok", vpath, ap) | ||||
|             return True | ||||
|         except: | ||||
|             # debug(" `-exists((%s)->(%s)):NOPE", vpath, ap) | ||||
|             return False | ||||
|  | ||||
|     def _p_getsize(self, vpath: str) -> int: | ||||
|         st = bos.stat(self._v2a("p.getsize", vpath)[1]) | ||||
|         st = bos.stat(self._v2a("p.getsize", vpath, perms=[True, False])[1]) | ||||
|         return st.st_size | ||||
|  | ||||
|     def _p_isdir(self, vpath: str) -> bool: | ||||
|         try: | ||||
|             st = bos.stat(self._v2a("p.isdir", vpath)[1]) | ||||
|             return stat.S_ISDIR(st.st_mode) | ||||
|             st = bos.stat(self._v2a("p.isdir", vpath, perms=[True, False])[1]) | ||||
|             ret = stat.S_ISDIR(st.st_mode) | ||||
|             # debug(" `-isdir:%s:%s", st.st_mode, ret) | ||||
|             return ret | ||||
|         except: | ||||
|             return False | ||||
|  | ||||
| @@ -303,6 +406,7 @@ class SMB(object): | ||||
|  | ||||
|         smbserver.os.path.abspath = self._hook | ||||
|         smbserver.os.path.expanduser = self._hook | ||||
|         smbserver.os.path.expandvars = self._hook | ||||
|         smbserver.os.path.getatime = self._hook | ||||
|         smbserver.os.path.getctime = self._hook | ||||
|         smbserver.os.path.getmtime = self._hook | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import errno | ||||
| import re | ||||
| import select | ||||
| import socket | ||||
| @@ -8,7 +9,7 @@ from email.utils import formatdate | ||||
|  | ||||
| from .__init__ import TYPE_CHECKING | ||||
| from .multicast import MC_Sck, MCast | ||||
| from .util import CachedSet, min_ex, html_escape | ||||
| from .util import CachedSet, html_escape, min_ex | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .broker_util import BrokerCli | ||||
| @@ -80,7 +81,7 @@ class SSDPr(object): | ||||
|         ubase = "{}://{}:{}".format(proto, sip, sport) | ||||
|         zsl = self.args.zsl | ||||
|         url = zsl if "://" in zsl else ubase + "/" + zsl.lstrip("/") | ||||
|         name = "{} @ {}".format(self.args.doctitle, self.args.name) | ||||
|         name = self.args.doctitle | ||||
|         zs = zs.strip().format(c(ubase), c(url), c(name), c(self.args.zsid)) | ||||
|         hc.reply(zs.encode("utf-8", "replace")) | ||||
|         return False  # close connectino | ||||
| @@ -129,6 +130,17 @@ class SSDPd(MCast): | ||||
|             srv.hport = hp | ||||
|  | ||||
|         self.log("listening") | ||||
|         try: | ||||
|             self.run2() | ||||
|         except OSError as ex: | ||||
|             if ex.errno != errno.EBADF: | ||||
|                 raise | ||||
|  | ||||
|             self.log("stopping due to {}".format(ex), "90") | ||||
|  | ||||
|         self.log("stopped", 2) | ||||
|  | ||||
|     def run2(self) -> None: | ||||
|         while self.running: | ||||
|             rdy = select.select(self.srv, [], [], self.args.z_chk or 180) | ||||
|             rx: list[socket.socket] = rdy[0]  # type: ignore | ||||
| @@ -148,8 +160,6 @@ class SSDPd(MCast): | ||||
|                     ) | ||||
|                     self.log(t, 6) | ||||
|  | ||||
|         self.log("stopped", 2) | ||||
|  | ||||
|     def stop(self) -> None: | ||||
|         self.running = False | ||||
|         for srv in self.srv.values(): | ||||
| @@ -204,7 +214,7 @@ CONFIGID.UPNP.ORG: 1 | ||||
|         srv.sck.sendto(zb, addr[:2]) | ||||
|  | ||||
|         if cip not in self.txc.c: | ||||
|             self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), "6") | ||||
|             self.log("{} [{}] --> {}".format(srv.name, srv.ip, cip), 6) | ||||
|  | ||||
|         self.txc.add(cip) | ||||
|         self.txc.cln() | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import re | ||||
| import stat | ||||
| import tarfile | ||||
|  | ||||
| @@ -43,24 +45,53 @@ class StreamTar(StreamArc): | ||||
|     def __init__( | ||||
|         self, | ||||
|         log: "NamedLogger", | ||||
|         args: argparse.Namespace, | ||||
|         fgen: Generator[dict[str, Any], None, None], | ||||
|         cmp: str = "", | ||||
|         **kwargs: Any | ||||
|     ): | ||||
|         super(StreamTar, self).__init__(log, fgen) | ||||
|         super(StreamTar, self).__init__(log, args, fgen) | ||||
|  | ||||
|         self.ci = 0 | ||||
|         self.co = 0 | ||||
|         self.qfile = QFile() | ||||
|         self.errf: dict[str, Any] = {} | ||||
|  | ||||
|         # python 3.8 changed to PAX_FORMAT as default, | ||||
|         # waste of space and don't care about the new features | ||||
|         # python 3.8 changed to PAX_FORMAT as default; | ||||
|         # slower, bigger, and no particular advantage | ||||
|         fmt = tarfile.GNU_FORMAT | ||||
|         self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt)  # type: ignore | ||||
|         if "pax" in cmp: | ||||
|             # unless a client asks for it (currently | ||||
|             # gnu-tar has wider support than pax-tar) | ||||
|             fmt = tarfile.PAX_FORMAT | ||||
|             cmp = re.sub(r"[^a-z0-9]*pax[^a-z0-9]*", "", cmp) | ||||
|  | ||||
|         try: | ||||
|             cmp, zs = cmp.replace(":", ",").split(",") | ||||
|             lv = int(zs) | ||||
|         except: | ||||
|             lv = -1 | ||||
|  | ||||
|         arg = {"name": None, "fileobj": self.qfile, "mode": "w", "format": fmt} | ||||
|         if cmp == "gz": | ||||
|             fun = tarfile.TarFile.gzopen | ||||
|             arg["compresslevel"] = lv if lv >= 0 else 3 | ||||
|         elif cmp == "bz2": | ||||
|             fun = tarfile.TarFile.bz2open | ||||
|             arg["compresslevel"] = lv if lv >= 0 else 2 | ||||
|         elif cmp == "xz": | ||||
|             fun = tarfile.TarFile.xzopen | ||||
|             arg["preset"] = lv if lv >= 0 else 1 | ||||
|         else: | ||||
|             fun = tarfile.open | ||||
|             arg["mode"] = "w|" | ||||
|  | ||||
|         self.tar = fun(**arg) | ||||
|  | ||||
|         Daemon(self._gen, "star-gen") | ||||
|  | ||||
|     def gen(self) -> Generator[Optional[bytes], None, None]: | ||||
|         buf = b"" | ||||
|         try: | ||||
|             while True: | ||||
|                 buf = self.qfile.q.get() | ||||
| @@ -72,6 +103,12 @@ class StreamTar(StreamArc): | ||||
|  | ||||
|             yield None | ||||
|         finally: | ||||
|             while buf: | ||||
|                 try: | ||||
|                     buf = self.qfile.q.get() | ||||
|                 except: | ||||
|                     pass | ||||
|  | ||||
|             if self.errf: | ||||
|                 bos.unlink(self.errf["ap"]) | ||||
|  | ||||
| @@ -91,7 +128,7 @@ class StreamTar(StreamArc): | ||||
|         inf.gid = 0 | ||||
|  | ||||
|         self.ci += inf.size | ||||
|         with open(fsenc(src), "rb", 512 * 1024) as fo: | ||||
|         with open(fsenc(src), "rb", self.args.iobuf) as fo: | ||||
|             self.tar.addfile(inf, fo) | ||||
|  | ||||
|     def _gen(self) -> None: | ||||
| @@ -101,6 +138,9 @@ class StreamTar(StreamArc): | ||||
|                 errors.append((f["vp"], f["err"])) | ||||
|                 continue | ||||
|  | ||||
|             if self.stopped: | ||||
|                 break | ||||
|  | ||||
|             try: | ||||
|                 self.ser(f) | ||||
|             except: | ||||
|   | ||||
| @@ -61,7 +61,7 @@ class Adapter(object): | ||||
|         ) | ||||
|  | ||||
|  | ||||
| if True: | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     # Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format) | ||||
|     _IPv4Address = str | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,15 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import os | ||||
| import tempfile | ||||
| from datetime import datetime | ||||
|  | ||||
| from .__init__ import CORES | ||||
| from .bos import bos | ||||
| from .th_cli import ThumbCli | ||||
| from .util import UTC, vjoin | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Any, Generator, Optional | ||||
| @@ -16,15 +21,87 @@ class StreamArc(object): | ||||
|     def __init__( | ||||
|         self, | ||||
|         log: "NamedLogger", | ||||
|         args: argparse.Namespace, | ||||
|         fgen: Generator[dict[str, Any], None, None], | ||||
|         **kwargs: Any | ||||
|     ): | ||||
|         self.log = log | ||||
|         self.args = args | ||||
|         self.fgen = fgen | ||||
|         self.stopped = False | ||||
|  | ||||
|     def gen(self) -> Generator[Optional[bytes], None, None]: | ||||
|         raise Exception("override me") | ||||
|  | ||||
|     def stop(self) -> None: | ||||
|         self.stopped = True | ||||
|  | ||||
|  | ||||
| def gfilter( | ||||
|     fgen: Generator[dict[str, Any], None, None], | ||||
|     thumbcli: ThumbCli, | ||||
|     uname: str, | ||||
|     vtop: str, | ||||
|     fmt: str, | ||||
| ) -> Generator[dict[str, Any], None, None]: | ||||
|     from concurrent.futures import ThreadPoolExecutor | ||||
|  | ||||
|     pend = [] | ||||
|     with ThreadPoolExecutor(max_workers=CORES) as tp: | ||||
|         try: | ||||
|             for f in fgen: | ||||
|                 task = tp.submit(enthumb, thumbcli, uname, vtop, f, fmt) | ||||
|                 pend.append((task, f)) | ||||
|                 if pend[0][0].done() or len(pend) > CORES * 4: | ||||
|                     task, f = pend.pop(0) | ||||
|                     try: | ||||
|                         f = task.result(600) | ||||
|                     except: | ||||
|                         pass | ||||
|                     yield f | ||||
|  | ||||
|             for task, f in pend: | ||||
|                 try: | ||||
|                     f = task.result(600) | ||||
|                 except: | ||||
|                     pass | ||||
|                 yield f | ||||
|         except Exception as ex: | ||||
|             thumbcli.log("gfilter flushing ({})".format(ex)) | ||||
|             for task, f in pend: | ||||
|                 try: | ||||
|                     task.result(600) | ||||
|                 except: | ||||
|                     pass | ||||
|             thumbcli.log("gfilter flushed") | ||||
|  | ||||
|  | ||||
| def enthumb( | ||||
|     thumbcli: ThumbCli, uname: str, vtop: str, f: dict[str, Any], fmt: str | ||||
| ) -> dict[str, Any]: | ||||
|     rem = f["vp"] | ||||
|     ext = rem.rsplit(".", 1)[-1].lower() | ||||
|     if (fmt == "mp3" and ext == "mp3") or ( | ||||
|         fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|") | ||||
|     ): | ||||
|         raise Exception() | ||||
|  | ||||
|     vp = vjoin(vtop, rem.split("/", 1)[1]) | ||||
|     vn, rem = thumbcli.asrv.vfs.get(vp, uname, True, False) | ||||
|     dbv, vrem = vn.get_dbv(rem) | ||||
|     thp = thumbcli.get(dbv, vrem, f["st"].st_mtime, fmt) | ||||
|     if not thp: | ||||
|         raise Exception() | ||||
|  | ||||
|     ext = "jpg" if fmt == "j" else "webp" if fmt == "w" else fmt | ||||
|     sz = bos.path.getsize(thp) | ||||
|     st: os.stat_result = f["st"] | ||||
|     ts = st.st_mtime | ||||
|     f["ap"] = thp | ||||
|     f["vp"] = f["vp"].rsplit(".", 1)[0] + "." + ext | ||||
|     f["st"] = os.stat_result((st.st_mode, -1, -1, 1, 1000, 1000, sz, ts, ts, ts)) | ||||
|     return f | ||||
|  | ||||
|  | ||||
| def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]: | ||||
|     report = ["copyparty failed to add the following files to the archive:", ""] | ||||
| @@ -36,7 +113,7 @@ def errdesc(errors: list[tuple[str, str]]) -> tuple[dict[str, Any], list[str]]: | ||||
|         tf_path = tf.name | ||||
|         tf.write("\r\n".join(report).encode("utf-8", "replace")) | ||||
|  | ||||
|     dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S") | ||||
|     dt = datetime.now(UTC).strftime("%Y-%m%d-%H%M%S") | ||||
|  | ||||
|     bos.chmod(tf_path, 0o444) | ||||
|     return { | ||||
|   | ||||
| @@ -28,22 +28,32 @@ if True:  # pylint: disable=using-constant-test | ||||
|     import typing | ||||
|     from typing import Any, Optional, Union | ||||
|  | ||||
| from .__init__ import ANYWIN, MACOS, TYPE_CHECKING, VT100, EnvParams, unicode | ||||
| from .authsrv import AuthSrv | ||||
| from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, E, EnvParams, unicode | ||||
| from .authsrv import BAD_CFG, AuthSrv | ||||
| from .cert import ensure_cert | ||||
| from .mtag import HAVE_FFMPEG, HAVE_FFPROBE | ||||
| from .tcpsrv import TcpSrv | ||||
| from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv | ||||
| from .up2k import Up2k | ||||
| from .util import ( | ||||
|     DEF_EXP, | ||||
|     DEF_MTE, | ||||
|     DEF_MTH, | ||||
|     FFMPEG_URL, | ||||
|     UTC, | ||||
|     VERSIONS, | ||||
|     Daemon, | ||||
|     Garda, | ||||
|     HLog, | ||||
|     HMaccas, | ||||
|     ODict, | ||||
|     alltrace, | ||||
|     ansi_re, | ||||
|     build_netmap, | ||||
|     min_ex, | ||||
|     mp, | ||||
|     odfusion, | ||||
|     pybin, | ||||
|     start_log_thrs, | ||||
|     start_stackmon, | ||||
| ) | ||||
| @@ -67,17 +77,25 @@ class SvcHub(object): | ||||
|     put() can return a queue (if want_reply=True) which has a blocking get() with the response. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, args: argparse.Namespace, argv: list[str], printed: str) -> None: | ||||
|     def __init__( | ||||
|         self, | ||||
|         args: argparse.Namespace, | ||||
|         dargs: argparse.Namespace, | ||||
|         argv: list[str], | ||||
|         printed: str, | ||||
|     ) -> None: | ||||
|         self.args = args | ||||
|         self.dargs = dargs | ||||
|         self.argv = argv | ||||
|         self.E: EnvParams = args.E | ||||
|         self.no_ansi = args.no_ansi | ||||
|         self.logf: Optional[typing.TextIO] = None | ||||
|         self.logf_base_fn = "" | ||||
|         self.stop_req = False | ||||
|         self.stopping = False | ||||
|         self.stopped = False | ||||
|         self.reload_req = False | ||||
|         self.reloading = False | ||||
|         self.reloading = 0 | ||||
|         self.stop_cond = threading.Condition() | ||||
|         self.nsigs = 3 | ||||
|         self.retcode = 0 | ||||
| @@ -89,11 +107,6 @@ class SvcHub(object): | ||||
|  | ||||
|         self.iphash = HMaccas(os.path.join(self.E.cfg, "iphash"), 8) | ||||
|  | ||||
|         # for non-http clients (ftp) | ||||
|         self.bans: dict[str, int] = {} | ||||
|         self.gpwd = Garda(self.args.ban_pw) | ||||
|         self.g404 = Garda(self.args.ban_404) | ||||
|  | ||||
|         if args.sss or args.s >= 3: | ||||
|             args.ss = True | ||||
|             args.no_dav = True | ||||
| @@ -109,7 +122,6 @@ class SvcHub(object): | ||||
|             args.no_mv = True | ||||
|             args.hardlink = True | ||||
|             args.vague_403 = True | ||||
|             args.ban_404 = "50,60,1440" | ||||
|             args.nih = True | ||||
|  | ||||
|         if args.s: | ||||
| @@ -119,6 +131,21 @@ class SvcHub(object): | ||||
|             args.no_robots = True | ||||
|             args.force_js = True | ||||
|  | ||||
|         if not self._process_config(): | ||||
|             raise Exception(BAD_CFG) | ||||
|  | ||||
|         # for non-http clients (ftp, tftp) | ||||
|         self.bans: dict[str, int] = {} | ||||
|         self.gpwd = Garda(self.args.ban_pw) | ||||
|         self.g404 = Garda(self.args.ban_404) | ||||
|         self.g403 = Garda(self.args.ban_403) | ||||
|         self.g422 = Garda(self.args.ban_422, False) | ||||
|         self.gmal = Garda(self.args.ban_422) | ||||
|         self.gurl = Garda(self.args.ban_url) | ||||
|  | ||||
|         self.log_div = 10 ** (6 - args.log_tdec) | ||||
|         self.log_efmt = "%02d:%02d:%02d.%0{}d".format(args.log_tdec) | ||||
|         self.log_dfmt = "%04d-%04d-%06d.%0{}d".format(args.log_tdec) | ||||
|         self.log = self._log_disabled if args.q else self._log_enabled | ||||
|         if args.lo: | ||||
|             self._setup_logfile(printed) | ||||
| @@ -128,6 +155,8 @@ class SvcHub(object): | ||||
|         lg.handlers = [lh] | ||||
|         lg.setLevel(logging.DEBUG) | ||||
|  | ||||
|         self._check_env() | ||||
|  | ||||
|         if args.stackmon: | ||||
|             start_stackmon(args.stackmon, 0) | ||||
|  | ||||
| @@ -140,22 +169,54 @@ class SvcHub(object): | ||||
|             self.log("root", t.format(args.j)) | ||||
|  | ||||
|         if not args.no_fpool and args.j != 1: | ||||
|             t = "WARNING: --use-fpool combined with multithreading is untested and can probably cause undefined behavior" | ||||
|             if ANYWIN: | ||||
|                 t = 'windows cannot do multithreading without --no-fpool, so enabling that -- note that upload performance will suffer if you have microsoft defender "real-time protection" enabled, so you probably want to use -j 1 instead' | ||||
|                 args.no_fpool = True | ||||
|             t = "WARNING: ignoring --use-fpool because multithreading (-j{}) is enabled" | ||||
|             self.log("root", t.format(args.j), c=3) | ||||
|             args.no_fpool = True | ||||
|  | ||||
|             self.log("root", t, c=3) | ||||
|         for name, arg in ( | ||||
|             ("iobuf", "iobuf"), | ||||
|             ("s-rd-sz", "s_rd_sz"), | ||||
|             ("s-wr-sz", "s_wr_sz"), | ||||
|         ): | ||||
|             zi = getattr(args, arg) | ||||
|             if zi < 32768: | ||||
|                 t = "WARNING: expect very poor performance because you specified a very low value (%d) for --%s" | ||||
|                 self.log("root", t % (zi, name), 3) | ||||
|                 zi = 2 | ||||
|             zi2 = 2 ** (zi - 1).bit_length() | ||||
|             if zi != zi2: | ||||
|                 zi3 = 2 ** ((zi - 1).bit_length() - 1) | ||||
|                 t = "WARNING: expect poor performance because --%s is not a power-of-two; consider using %d or %d instead of %d" | ||||
|                 self.log("root", t % (name, zi2, zi3, zi), 3) | ||||
|  | ||||
|         if args.s_rd_sz > args.iobuf: | ||||
|             t = "WARNING: --s-rd-sz (%d) is larger than --iobuf (%d); this may lead to reduced performance" | ||||
|             self.log("root", t % (args.s_rd_sz, args.iobuf), 3) | ||||
|  | ||||
|         bri = "zy"[args.theme % 2 :][:1] | ||||
|         ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)] | ||||
|         args.theme = "{0}{1} {0} {1}".format(ch, bri) | ||||
|  | ||||
|         if args.nih: | ||||
|             args.vname = "" | ||||
|             args.doctitle = args.doctitle.replace(" @ --name", "") | ||||
|         else: | ||||
|             args.vname = args.name | ||||
|         args.doctitle = args.doctitle.replace("--name", args.vname) | ||||
|         args.bname = args.bname.replace("--name", args.vname) or args.vname | ||||
|  | ||||
|         if args.log_fk: | ||||
|             args.log_fk = re.compile(args.log_fk) | ||||
|  | ||||
|         # initiate all services to manage | ||||
|         self.asrv = AuthSrv(self.args, self.log) | ||||
|         self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs) | ||||
|  | ||||
|         if args.cgen: | ||||
|             self.asrv.cgen() | ||||
|  | ||||
|         if args.exit == "cfg": | ||||
|             sys.exit(0) | ||||
|  | ||||
|         if args.ls: | ||||
|             self.asrv.dbg_ls() | ||||
|  | ||||
| @@ -164,10 +225,11 @@ class SvcHub(object): | ||||
|  | ||||
|         self.log("root", "max clients: {}".format(self.args.nc)) | ||||
|  | ||||
|         if not self._process_config(): | ||||
|             raise Exception("bad config") | ||||
|  | ||||
|         self.tcpsrv = TcpSrv(self) | ||||
|  | ||||
|         if not self.tcpsrv.srv and self.args.ign_ebind_all: | ||||
|             self.args.no_fastboot = True | ||||
|  | ||||
|         self.up2k = Up2k(self) | ||||
|  | ||||
|         decs = {k: 1 for k in self.args.th_dec.split(",")} | ||||
| @@ -178,8 +240,13 @@ class SvcHub(object): | ||||
|         if not HAVE_FFMPEG or not HAVE_FFPROBE: | ||||
|             decs.pop("ff", None) | ||||
|  | ||||
|         # compressed formats; "s3z=s3m.zip, s3gz=s3m.gz, ..." | ||||
|         zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")] | ||||
|         args.au_unpk = {x[0]: x[1] for x in zlss} | ||||
|  | ||||
|         self.args.th_dec = list(decs.keys()) | ||||
|         self.thumbsrv = None | ||||
|         want_ff = False | ||||
|         if not args.no_thumb: | ||||
|             t = ", ".join(self.args.th_dec) or "(None available)" | ||||
|             self.log("thumb", "decoder preference: {}".format(t)) | ||||
| @@ -191,8 +258,12 @@ class SvcHub(object): | ||||
|             if self.args.th_dec: | ||||
|                 self.thumbsrv = ThumbSrv(self) | ||||
|             else: | ||||
|                 want_ff = True | ||||
|                 msg = "need either Pillow, pyvips, or FFmpeg to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n{0}apt install ffmpeg" | ||||
|                 msg = msg.format(" " * 37, os.path.basename(sys.executable)) | ||||
|                 msg = msg.format(" " * 37, os.path.basename(pybin)) | ||||
|                 if EXE: | ||||
|                     msg = "copyparty.exe cannot use Pillow or pyvips; need ffprobe.exe and ffmpeg.exe to create thumbnails" | ||||
|  | ||||
|                 self.log("thumb", msg, c=3) | ||||
|  | ||||
|         if not args.no_acode and args.no_thumb: | ||||
| @@ -204,6 +275,17 @@ class SvcHub(object): | ||||
|             msg = "setting --no-acode because either FFmpeg or FFprobe is not available" | ||||
|             self.log("thumb", msg, c=6) | ||||
|             args.no_acode = True | ||||
|             want_ff = True | ||||
|  | ||||
|         if want_ff and ANYWIN: | ||||
|             self.log("thumb", "download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) | ||||
|  | ||||
|         if not args.no_acode: | ||||
|             if not re.match("^(0|[qv][0-9]|[0-9]{2,3}k)$", args.q_mp3.lower()): | ||||
|                 t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]" | ||||
|                 raise Exception(t % (args.q_mp3,)) | ||||
|         else: | ||||
|             args.au_unpk = {} | ||||
|  | ||||
|         args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage) | ||||
|  | ||||
| @@ -216,9 +298,17 @@ class SvcHub(object): | ||||
|         if args.ftp or args.ftps: | ||||
|             from .ftpd import Ftpd | ||||
|  | ||||
|             self.ftpd = Ftpd(self) | ||||
|             self.ftpd: Optional[Ftpd] = None | ||||
|             zms += "f" if args.ftp else "F" | ||||
|  | ||||
|         if args.tftp: | ||||
|             from .tftpd import Tftpd | ||||
|  | ||||
|             self.tftpd: Optional[Tftpd] = None | ||||
|  | ||||
|         if args.ftp or args.ftps or args.tftp: | ||||
|             Daemon(self.start_ftpd, "start_tftpd") | ||||
|  | ||||
|         if args.smb: | ||||
|             # impacket.dcerpc is noisy about listen timeouts | ||||
|             sto = socket.getdefaulttimeout() | ||||
| @@ -246,6 +336,41 @@ class SvcHub(object): | ||||
|  | ||||
|         self.broker = Broker(self) | ||||
|  | ||||
|     def start_ftpd(self) -> None: | ||||
|         time.sleep(30) | ||||
|  | ||||
|         if hasattr(self, "ftpd") and not self.ftpd: | ||||
|             self.restart_ftpd() | ||||
|  | ||||
|         if hasattr(self, "tftpd") and not self.tftpd: | ||||
|             self.restart_tftpd() | ||||
|  | ||||
|     def restart_ftpd(self) -> None: | ||||
|         if not hasattr(self, "ftpd"): | ||||
|             return | ||||
|  | ||||
|         from .ftpd import Ftpd | ||||
|  | ||||
|         if self.ftpd: | ||||
|             return  # todo | ||||
|  | ||||
|         if not os.path.exists(self.args.cert): | ||||
|             ensure_cert(self.log, self.args) | ||||
|  | ||||
|         self.ftpd = Ftpd(self) | ||||
|         self.log("root", "started FTPd") | ||||
|  | ||||
|     def restart_tftpd(self) -> None: | ||||
|         if not hasattr(self, "tftpd"): | ||||
|             return | ||||
|  | ||||
|         from .tftpd import Tftpd | ||||
|  | ||||
|         if self.tftpd: | ||||
|             return  # todo | ||||
|  | ||||
|         self.tftpd = Tftpd(self) | ||||
|  | ||||
|     def thr_httpsrv_up(self) -> None: | ||||
|         time.sleep(1 if self.args.ign_ebind_all else 5) | ||||
|         expected = self.broker.num_workers * self.tcpsrv.nsrv | ||||
| @@ -270,23 +395,48 @@ class SvcHub(object): | ||||
|         self.sigterm() | ||||
|  | ||||
|     def sigterm(self) -> None: | ||||
|         os.kill(os.getpid(), signal.SIGTERM) | ||||
|         self.signal_handler(signal.SIGTERM, None) | ||||
|  | ||||
|     def cb_httpsrv_up(self) -> None: | ||||
|         self.httpsrv_up += 1 | ||||
|         if self.httpsrv_up != self.broker.num_workers: | ||||
|             return | ||||
|  | ||||
|         time.sleep(0.1)  # purely cosmetic dw | ||||
|         ar = self.args | ||||
|         for _ in range(10 if ar.ftp or ar.ftps else 0): | ||||
|             time.sleep(0.03) | ||||
|             if self.ftpd: | ||||
|                 break | ||||
|  | ||||
|         if self.tcpsrv.qr: | ||||
|             self.log("qr-code", self.tcpsrv.qr) | ||||
|         else: | ||||
|             self.log("root", "workers OK\n") | ||||
|  | ||||
|         self.after_httpsrv_up() | ||||
|  | ||||
|     def after_httpsrv_up(self) -> None: | ||||
|         self.up2k.init_vols() | ||||
|  | ||||
|         Daemon(self.sd_notify, "sd-notify") | ||||
|  | ||||
|     def _check_env(self) -> None: | ||||
|         try: | ||||
|             files = os.listdir(E.cfg) | ||||
|         except: | ||||
|             files = [] | ||||
|  | ||||
|         hits = [x for x in files if x.lower().endswith(".conf")] | ||||
|         if hits: | ||||
|             t = "WARNING: found config files in [%s]: %s\n  config files are not expected here, and will NOT be loaded (unless your setup is intentionally hella funky)" | ||||
|             self.log("root", t % (E.cfg, ", ".join(hits)), 3) | ||||
|  | ||||
|         if self.args.no_bauth: | ||||
|             t = "WARNING: --no-bauth disables support for the Android app; you may want to use --bauth-last instead" | ||||
|             self.log("root", t, 3) | ||||
|             if self.args.bauth_last: | ||||
|                 self.log("root", "WARNING: ignoring --bauth-last due to --no-bauth", 3) | ||||
|  | ||||
|     def _process_config(self) -> bool: | ||||
|         al = self.args | ||||
|  | ||||
| @@ -323,14 +473,116 @@ class SvcHub(object): | ||||
|         al.RS = R + "/" if R else "" | ||||
|         al.SRS = "/" + R + "/" if R else "/" | ||||
|  | ||||
|         if al.rsp_jtr: | ||||
|             al.rsp_slp = 0.000001 | ||||
|  | ||||
|         zsl = al.th_covers.split(",") | ||||
|         zsl = [x.strip() for x in zsl] | ||||
|         zsl = [x for x in zsl if x] | ||||
|         al.th_covers = set(zsl) | ||||
|         al.th_coversd = set(zsl + ["." + x for x in zsl]) | ||||
|  | ||||
|         for k in "c".split(" "): | ||||
|             vl = getattr(al, k) | ||||
|             if not vl: | ||||
|                 continue | ||||
|  | ||||
|             vl = [os.path.expandvars(os.path.expanduser(x)) for x in vl] | ||||
|             setattr(al, k, vl) | ||||
|  | ||||
|         for k in "lo hist ssl_log".split(" "): | ||||
|             vs = getattr(al, k) | ||||
|             if vs: | ||||
|                 vs = os.path.expandvars(os.path.expanduser(vs)) | ||||
|                 setattr(al, k, vs) | ||||
|  | ||||
|         for k in "sus_urls nonsus_urls".split(" "): | ||||
|             vs = getattr(al, k) | ||||
|             if not vs or vs == "no": | ||||
|                 setattr(al, k, None) | ||||
|             else: | ||||
|                 setattr(al, k, re.compile(vs)) | ||||
|  | ||||
|         for k in "tftp_lsf".split(" "): | ||||
|             vs = getattr(al, k) | ||||
|             if not vs or vs == "no": | ||||
|                 setattr(al, k, None) | ||||
|             else: | ||||
|                 setattr(al, k, re.compile("^" + vs + "$")) | ||||
|  | ||||
|         if not al.sus_urls: | ||||
|             al.ban_url = "no" | ||||
|         elif al.ban_url == "no": | ||||
|             al.sus_urls = None | ||||
|  | ||||
|         al.xff_hdr = al.xff_hdr.lower() | ||||
|         al.idp_h_usr = al.idp_h_usr.lower() | ||||
|         al.idp_h_grp = al.idp_h_grp.lower() | ||||
|         al.idp_h_key = al.idp_h_key.lower() | ||||
|  | ||||
|         al.ftp_ipa_nm = build_netmap(al.ftp_ipa or al.ipa) | ||||
|         al.tftp_ipa_nm = build_netmap(al.tftp_ipa or al.ipa) | ||||
|  | ||||
|         mte = ODict.fromkeys(DEF_MTE.split(","), True) | ||||
|         al.mte = odfusion(mte, al.mte) | ||||
|  | ||||
|         mth = ODict.fromkeys(DEF_MTH.split(","), True) | ||||
|         al.mth = odfusion(mth, al.mth) | ||||
|  | ||||
|         exp = ODict.fromkeys(DEF_EXP.split(" "), True) | ||||
|         al.exp_md = odfusion(exp, al.exp_md.replace(" ", ",")) | ||||
|         al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ",")) | ||||
|  | ||||
|         for k in ["no_hash", "no_idx", "og_ua"]: | ||||
|             ptn = getattr(self.args, k) | ||||
|             if ptn: | ||||
|                 setattr(self.args, k, re.compile(ptn)) | ||||
|  | ||||
|         for k in ["idp_gsep"]: | ||||
|             ptn = getattr(self.args, k) | ||||
|             if "]" in ptn: | ||||
|                 ptn = "]" + ptn.replace("]", "") | ||||
|             if "[" in ptn: | ||||
|                 ptn = ptn.replace("[", "") + "[" | ||||
|             if "-" in ptn: | ||||
|                 ptn = ptn.replace("-", "") + "-" | ||||
|  | ||||
|             ptn = ptn.replace("\\", "\\\\").replace("^", "\\^") | ||||
|             setattr(self.args, k, re.compile("[%s]" % (ptn,))) | ||||
|  | ||||
|         try: | ||||
|             zf1, zf2 = self.args.rm_retry.split("/") | ||||
|             self.args.rm_re_t = float(zf1) | ||||
|             self.args.rm_re_r = float(zf2) | ||||
|         except: | ||||
|             raise Exception("invalid --rm-retry [%s]" % (self.args.rm_retry,)) | ||||
|  | ||||
|         try: | ||||
|             zf1, zf2 = self.args.mv_retry.split("/") | ||||
|             self.args.mv_re_t = float(zf1) | ||||
|             self.args.mv_re_r = float(zf2) | ||||
|         except: | ||||
|             raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,)) | ||||
|  | ||||
|         al.tcolor = al.tcolor.lstrip("#") | ||||
|         if len(al.tcolor) == 3:  # fc5 => ffcc55 | ||||
|             al.tcolor = "".join([x * 2 for x in al.tcolor]) | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _ipa2re(self, txt) -> Optional[re.Pattern]: | ||||
|         if txt in ("any", "0", ""): | ||||
|             return None | ||||
|  | ||||
|         zs = txt.replace(" ", "").replace(".", "\\.").replace(",", "|") | ||||
|         return re.compile("^(?:" + zs + ")") | ||||
|  | ||||
|     def _setlimits(self) -> None: | ||||
|         try: | ||||
|             import resource | ||||
|  | ||||
|             soft, hard = [ | ||||
|                 x if x > 0 else 1024 * 1024 | ||||
|                 int(x) if x > 0 else 1024 * 1024 | ||||
|                 for x in list(resource.getrlimit(resource.RLIMIT_NOFILE)) | ||||
|             ] | ||||
|         except: | ||||
| @@ -366,7 +618,7 @@ class SvcHub(object): | ||||
|             self.args.nc = min(self.args.nc, soft // 2) | ||||
|  | ||||
|     def _logname(self) -> str: | ||||
|         dt = datetime.utcnow() | ||||
|         dt = datetime.now(UTC) | ||||
|         fn = str(self.args.lo) | ||||
|         for fs in "YmdHMS": | ||||
|             fs = "%" + fs | ||||
| @@ -377,6 +629,7 @@ class SvcHub(object): | ||||
|  | ||||
|     def _setup_logfile(self, printed: str) -> None: | ||||
|         base_fn = fn = sel_fn = self._logname() | ||||
|         do_xz = fn.lower().endswith(".xz") | ||||
|         if fn != self.args.lo: | ||||
|             ctr = 0 | ||||
|             # yup this is a race; if started sufficiently concurrently, two | ||||
| @@ -386,12 +639,17 @@ class SvcHub(object): | ||||
|                 sel_fn = "{}.{}".format(fn, ctr) | ||||
|  | ||||
|         fn = sel_fn | ||||
|         try: | ||||
|             os.makedirs(os.path.dirname(fn)) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         try: | ||||
|             if fn.lower().endswith(".xz"): | ||||
|             if do_xz: | ||||
|                 import lzma | ||||
|  | ||||
|                 lh = lzma.open(fn, "wt", encoding="utf-8", errors="replace", preset=0) | ||||
|                 self.args.no_logflush = True | ||||
|             else: | ||||
|                 lh = open(fn, "wt", encoding="utf-8", errors="replace") | ||||
|         except: | ||||
| @@ -399,7 +657,7 @@ class SvcHub(object): | ||||
|  | ||||
|             lh = codecs.open(fn, "w", encoding="utf-8", errors="replace") | ||||
|  | ||||
|         argv = [sys.executable] + self.argv | ||||
|         argv = [pybin] + self.argv | ||||
|         if hasattr(shlex, "quote"): | ||||
|             argv = [shlex.quote(x) for x in argv] | ||||
|         else: | ||||
| @@ -478,21 +736,37 @@ class SvcHub(object): | ||||
|                 self.log("root", "ssdp startup failed;\n" + min_ex(), 3) | ||||
|  | ||||
|     def reload(self) -> str: | ||||
|         if self.reloading: | ||||
|             return "cannot reload; already in progress" | ||||
|         with self.up2k.mutex: | ||||
|             if self.reloading: | ||||
|                 return "cannot reload; already in progress" | ||||
|             self.reloading = 1 | ||||
|  | ||||
|         self.reloading = True | ||||
|         Daemon(self._reload, "reloading") | ||||
|         return "reload initiated" | ||||
|  | ||||
|     def _reload(self) -> None: | ||||
|         self.log("root", "reload scheduled") | ||||
|     def _reload(self, rescan_all_vols: bool = True) -> None: | ||||
|         with self.up2k.mutex: | ||||
|             if self.reloading != 1: | ||||
|                 return | ||||
|             self.reloading = 2 | ||||
|             self.log("root", "reloading config") | ||||
|             self.asrv.reload() | ||||
|             self.up2k.reload() | ||||
|             self.up2k.reload(rescan_all_vols) | ||||
|             self.broker.reload() | ||||
|             self.reloading = 0 | ||||
|  | ||||
|         self.reloading = False | ||||
|     def _reload_blocking(self, rescan_all_vols: bool = True) -> None: | ||||
|         while True: | ||||
|             with self.up2k.mutex: | ||||
|                 if self.reloading < 2: | ||||
|                     self.reloading = 1 | ||||
|                     break | ||||
|             time.sleep(0.05) | ||||
|  | ||||
|         # try to handle multiple pending IdP reloads at once: | ||||
|         time.sleep(0.2) | ||||
|  | ||||
|         self._reload(rescan_all_vols=rescan_all_vols) | ||||
|  | ||||
|     def stop_thr(self) -> None: | ||||
|         while not self.stop_req: | ||||
| @@ -552,19 +826,25 @@ class SvcHub(object): | ||||
|         ret = 1 | ||||
|         try: | ||||
|             self.pr("OPYTHAT") | ||||
|             tasks = [] | ||||
|             slp = 0.0 | ||||
|  | ||||
|             if self.mdns: | ||||
|                 Daemon(self.mdns.stop) | ||||
|                 tasks.append(Daemon(self.mdns.stop, "mdns")) | ||||
|                 slp = time.time() + 0.5 | ||||
|  | ||||
|             if self.ssdp: | ||||
|                 Daemon(self.ssdp.stop) | ||||
|                 tasks.append(Daemon(self.ssdp.stop, "ssdp")) | ||||
|                 slp = time.time() + 0.5 | ||||
|  | ||||
|             self.broker.shutdown() | ||||
|             self.tcpsrv.shutdown() | ||||
|             self.up2k.shutdown() | ||||
|  | ||||
|             if hasattr(self, "smbd"): | ||||
|                 slp = max(slp, time.time() + 0.5) | ||||
|                 tasks.append(Daemon(self.smbd.stop, "smbd")) | ||||
|  | ||||
|             if self.thumbsrv: | ||||
|                 self.thumbsrv.shutdown() | ||||
|  | ||||
| @@ -574,17 +854,19 @@ class SvcHub(object): | ||||
|                         break | ||||
|  | ||||
|                     if n == 3: | ||||
|                         self.pr("waiting for thumbsrv (10sec)...") | ||||
|                         self.log("root", "waiting for thumbsrv (10sec)...") | ||||
|  | ||||
|             if hasattr(self, "smbd"): | ||||
|                 slp = max(slp, time.time() + 0.5) | ||||
|                 Daemon(self.kill9, a=(1,)) | ||||
|                 Daemon(self.smbd.stop) | ||||
|                 zf = max(time.time() - slp, 0) | ||||
|                 Daemon(self.kill9, a=(zf + 0.5,)) | ||||
|  | ||||
|             while time.time() < slp: | ||||
|                 time.sleep(0.1) | ||||
|                 if not next((x for x in tasks if x.is_alive), None): | ||||
|                     break | ||||
|  | ||||
|             self.pr("nailed it", end="") | ||||
|                 time.sleep(0.05) | ||||
|  | ||||
|             self.log("root", "nailed it") | ||||
|             ret = self.retcode | ||||
|         except: | ||||
|             self.pr("\033[31m[ error during shutdown ]\n{}\033[0m".format(min_ex())) | ||||
| @@ -594,7 +876,7 @@ class SvcHub(object): | ||||
|                 print("\033]0;\033\\", file=sys.stderr, end="") | ||||
|                 sys.stderr.flush() | ||||
|  | ||||
|             self.pr("\033[0m") | ||||
|             self.pr("\033[0m", end="") | ||||
|             if self.logf: | ||||
|                 self.logf.close() | ||||
|  | ||||
| @@ -606,11 +888,34 @@ class SvcHub(object): | ||||
|             return | ||||
|  | ||||
|         with self.log_mutex: | ||||
|             ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3] | ||||
|             self.logf.write("@{} [{}\033[0m] {}\n".format(ts, src, msg)) | ||||
|             zd = datetime.now(UTC) | ||||
|             ts = self.log_dfmt % ( | ||||
|                 zd.year, | ||||
|                 zd.month * 100 + zd.day, | ||||
|                 (zd.hour * 100 + zd.minute) * 100 + zd.second, | ||||
|                 zd.microsecond // self.log_div, | ||||
|             ) | ||||
|  | ||||
|             if c and not self.args.no_ansi: | ||||
|                 if isinstance(c, int): | ||||
|                     msg = "\033[3%sm%s\033[0m" % (c, msg) | ||||
|                 elif "\033" not in c: | ||||
|                     msg = "\033[%sm%s\033[0m" % (c, msg) | ||||
|                 else: | ||||
|                     msg = "%s%s\033[0m" % (c, msg) | ||||
|  | ||||
|             if "\033" in src: | ||||
|                 src += "\033[0m" | ||||
|  | ||||
|             if "\033" in msg: | ||||
|                 msg += "\033[0m" | ||||
|  | ||||
|             self.logf.write("@%s [%-21s] %s\n" % (ts, src, msg)) | ||||
|             if not self.args.no_logflush: | ||||
|                 self.logf.flush() | ||||
|  | ||||
|             now = time.time() | ||||
|             if now >= self.next_day: | ||||
|             if int(now) >= self.next_day: | ||||
|                 self._set_next_day() | ||||
|  | ||||
|     def _set_next_day(self) -> None: | ||||
| @@ -618,7 +923,7 @@ class SvcHub(object): | ||||
|             self.logf.close() | ||||
|             self._setup_logfile("") | ||||
|  | ||||
|         dt = datetime.utcnow() | ||||
|         dt = datetime.now(UTC) | ||||
|  | ||||
|         # unix timestamp of next 00:00:00 (leap-seconds safe) | ||||
|         day_now = dt.day | ||||
| @@ -626,34 +931,50 @@ class SvcHub(object): | ||||
|             dt += timedelta(hours=12) | ||||
|  | ||||
|         dt = dt.replace(hour=0, minute=0, second=0) | ||||
|         self.next_day = calendar.timegm(dt.utctimetuple()) | ||||
|         try: | ||||
|             tt = dt.utctimetuple() | ||||
|         except: | ||||
|             # still makes me hella uncomfortable | ||||
|             tt = dt.timetuple() | ||||
|  | ||||
|         self.next_day = calendar.timegm(tt) | ||||
|  | ||||
|     def _log_enabled(self, src: str, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         """handles logging from all components""" | ||||
|         with self.log_mutex: | ||||
|             now = time.time() | ||||
|             if now >= self.next_day: | ||||
|                 dt = datetime.utcfromtimestamp(now) | ||||
|                 print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="") | ||||
|             if int(now) >= self.next_day: | ||||
|                 dt = datetime.fromtimestamp(now, UTC) | ||||
|                 zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n" | ||||
|                 zs = zs.format(dt.strftime("%Y-%m-%d")) | ||||
|                 print(zs, end="") | ||||
|                 self._set_next_day() | ||||
|                 if self.logf: | ||||
|                     self.logf.write(zs) | ||||
|  | ||||
|             fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n" | ||||
|             if not VT100: | ||||
|                 fmt = "{} {:21} {}\n" | ||||
|             fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" | ||||
|             if self.no_ansi: | ||||
|                 fmt = "%s %-21s %s\n" | ||||
|                 if "\033" in msg: | ||||
|                     msg = ansi_re.sub("", msg) | ||||
|                 if "\033" in src: | ||||
|                     src = ansi_re.sub("", src) | ||||
|             elif c: | ||||
|                 if isinstance(c, int): | ||||
|                     msg = "\033[3{}m{}\033[0m".format(c, msg) | ||||
|                     msg = "\033[3%sm%s\033[0m" % (c, msg) | ||||
|                 elif "\033" not in c: | ||||
|                     msg = "\033[{}m{}\033[0m".format(c, msg) | ||||
|                     msg = "\033[%sm%s\033[0m" % (c, msg) | ||||
|                 else: | ||||
|                     msg = "{}{}\033[0m".format(c, msg) | ||||
|                     msg = "%s%s\033[0m" % (c, msg) | ||||
|  | ||||
|             ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3] | ||||
|             msg = fmt.format(ts, src, msg) | ||||
|             zd = datetime.fromtimestamp(now, UTC) | ||||
|             ts = self.log_efmt % ( | ||||
|                 zd.hour, | ||||
|                 zd.minute, | ||||
|                 zd.second, | ||||
|                 zd.microsecond // self.log_div, | ||||
|             ) | ||||
|             msg = fmt % (ts, src, msg) | ||||
|             try: | ||||
|                 print(msg, end="") | ||||
|             except UnicodeEncodeError: | ||||
| @@ -667,6 +988,8 @@ class SvcHub(object): | ||||
|  | ||||
|             if self.logf: | ||||
|                 self.logf.write(msg) | ||||
|                 if not self.args.no_logflush: | ||||
|                     self.logf.flush() | ||||
|  | ||||
|     def pr(self, *a: Any, **ka: Any) -> None: | ||||
|         try: | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import argparse | ||||
| import calendar | ||||
| import time | ||||
| import stat | ||||
| import time | ||||
| import zlib | ||||
|  | ||||
| from .bos import bos | ||||
| @@ -218,11 +219,13 @@ class StreamZip(StreamArc): | ||||
|     def __init__( | ||||
|         self, | ||||
|         log: "NamedLogger", | ||||
|         args: argparse.Namespace, | ||||
|         fgen: Generator[dict[str, Any], None, None], | ||||
|         utf8: bool = False, | ||||
|         pre_crc: bool = False, | ||||
|         **kwargs: Any | ||||
|     ) -> None: | ||||
|         super(StreamZip, self).__init__(log, fgen) | ||||
|         super(StreamZip, self).__init__(log, args, fgen) | ||||
|  | ||||
|         self.utf8 = utf8 | ||||
|         self.pre_crc = pre_crc | ||||
| @@ -247,7 +250,7 @@ class StreamZip(StreamArc): | ||||
|  | ||||
|         crc = 0 | ||||
|         if self.pre_crc: | ||||
|             for buf in yieldfile(src): | ||||
|             for buf in yieldfile(src, self.args.iobuf): | ||||
|                 crc = zlib.crc32(buf, crc) | ||||
|  | ||||
|             crc &= 0xFFFFFFFF | ||||
| @@ -256,7 +259,7 @@ class StreamZip(StreamArc): | ||||
|         buf = gen_hdr(None, name, sz, ts, self.utf8, crc, self.pre_crc) | ||||
|         yield self._ct(buf) | ||||
|  | ||||
|         for buf in yieldfile(src): | ||||
|         for buf in yieldfile(src, self.args.iobuf): | ||||
|             if not self.pre_crc: | ||||
|                 crc = zlib.crc32(buf, crc) | ||||
|  | ||||
| @@ -275,6 +278,7 @@ class StreamZip(StreamArc): | ||||
|     def gen(self) -> Generator[bytes, None, None]: | ||||
|         errf: dict[str, Any] = {} | ||||
|         errors = [] | ||||
|         mbuf = b"" | ||||
|         try: | ||||
|             for f in self.fgen: | ||||
|                 if "err" in f: | ||||
| @@ -283,13 +287,20 @@ class StreamZip(StreamArc): | ||||
|  | ||||
|                 try: | ||||
|                     for x in self.ser(f): | ||||
|                         yield x | ||||
|                         mbuf += x | ||||
|                         if len(mbuf) >= 16384: | ||||
|                             yield mbuf | ||||
|                             mbuf = b"" | ||||
|                 except GeneratorExit: | ||||
|                     raise | ||||
|                 except: | ||||
|                     ex = min_ex(5, True).replace("\n", "\n-- ") | ||||
|                     errors.append((f["vp"], ex)) | ||||
|  | ||||
|             if mbuf: | ||||
|                 yield mbuf | ||||
|                 mbuf = b"" | ||||
|  | ||||
|             if errors: | ||||
|                 errf, txt = errdesc(errors) | ||||
|                 self.log("\n".join(([repr(errf)] + txt[1:]))) | ||||
| @@ -299,20 +310,23 @@ class StreamZip(StreamArc): | ||||
|             cdir_pos = self.pos | ||||
|             for name, sz, ts, crc, h_pos in self.items: | ||||
|                 buf = gen_hdr(h_pos, name, sz, ts, self.utf8, crc, self.pre_crc) | ||||
|                 yield self._ct(buf) | ||||
|                 mbuf += self._ct(buf) | ||||
|                 if len(mbuf) >= 16384: | ||||
|                     yield mbuf | ||||
|                     mbuf = b"" | ||||
|             cdir_end = self.pos | ||||
|  | ||||
|             _, need_64 = gen_ecdr(self.items, cdir_pos, cdir_end) | ||||
|             if need_64: | ||||
|                 ecdir64_pos = self.pos | ||||
|                 buf = gen_ecdr64(self.items, cdir_pos, cdir_end) | ||||
|                 yield self._ct(buf) | ||||
|                 mbuf += self._ct(buf) | ||||
|  | ||||
|                 buf = gen_ecdr64_loc(ecdir64_pos) | ||||
|                 yield self._ct(buf) | ||||
|                 mbuf += self._ct(buf) | ||||
|  | ||||
|             ecdr, _ = gen_ecdr(self.items, cdir_pos, cdir_end) | ||||
|             yield self._ct(ecdr) | ||||
|             yield mbuf + self._ct(ecdr) | ||||
|         finally: | ||||
|             if errf: | ||||
|                 bos.unlink(errf["ap"]) | ||||
|   | ||||
| @@ -7,13 +7,15 @@ import socket | ||||
| import sys | ||||
| import time | ||||
|  | ||||
| from .__init__ import ANYWIN, PY2, TYPE_CHECKING, VT100, unicode | ||||
| from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode | ||||
| from .cert import gencert | ||||
| from .stolen.qrcodegen import QrCode | ||||
| from .util import ( | ||||
|     E_ACCESS, | ||||
|     E_ADDR_IN_USE, | ||||
|     E_ADDR_NOT_AVAIL, | ||||
|     E_UNREACH, | ||||
|     IP6ALL, | ||||
|     Netdev, | ||||
|     min_ex, | ||||
|     sunpack, | ||||
| @@ -239,6 +241,11 @@ class TcpSrv(object): | ||||
|                 raise OSError(E_ADDR_IN_USE[0], "") | ||||
|             self.srv.append(srv) | ||||
|         except (OSError, socket.error) as ex: | ||||
|             try: | ||||
|                 srv.close() | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|             if ex.errno in E_ADDR_IN_USE: | ||||
|                 e = "\033[1;31mport {} is busy on interface {}\033[0m".format(port, ip) | ||||
|             elif ex.errno in E_ADDR_NOT_AVAIL: | ||||
| @@ -253,6 +260,9 @@ class TcpSrv(object): | ||||
|         srvs: list[socket.socket] = [] | ||||
|         for srv in self.srv: | ||||
|             ip, port = srv.getsockname()[:2] | ||||
|             if ip == IP6ALL: | ||||
|                 ip = "::"  # jython | ||||
|  | ||||
|             try: | ||||
|                 srv.listen(self.args.nc) | ||||
|                 try: | ||||
| @@ -274,6 +284,8 @@ class TcpSrv(object): | ||||
|                     srv.close() | ||||
|                     continue | ||||
|  | ||||
|                 t = "\n\nERROR: could not open listening socket, probably because one of the server ports ({}) is busy on one of the requested interfaces ({}); avoid this issue by specifying a different port (-p 3939) and/or a specific interface to listen on (-i 192.168.56.1)\n" | ||||
|                 self.log("tcpsrv", t.format(port, ip), 1) | ||||
|                 raise | ||||
|  | ||||
|             bound.append((ip, port)) | ||||
| @@ -295,6 +307,9 @@ class TcpSrv(object): | ||||
|     def _distribute_netdevs(self): | ||||
|         self.hub.broker.say("set_netdevs", self.netdevs) | ||||
|         self.hub.start_zeroconf() | ||||
|         gencert(self.log, self.args, self.netdevs) | ||||
|         self.hub.restart_ftpd() | ||||
|         self.hub.restart_tftpd() | ||||
|  | ||||
|     def shutdown(self) -> None: | ||||
|         self.stopping = True | ||||
| @@ -322,7 +337,7 @@ class TcpSrv(object): | ||||
|                 if k not in netdevs: | ||||
|                     removed = "{} = {}".format(k, v) | ||||
|  | ||||
|             t = "network change detected:\n  added {}\nremoved {}" | ||||
|             t = "network change detected:\n  added {}\033[0;33m\nremoved {}" | ||||
|             self.log("tcpsrv", t.format(added, removed), 3) | ||||
|             self.netdevs = netdevs | ||||
|             self._distribute_netdevs() | ||||
| @@ -448,6 +463,12 @@ class TcpSrv(object): | ||||
|         sys.stderr.flush() | ||||
|  | ||||
|     def _qr(self, t1: dict[str, list[int]], t2: dict[str, list[int]]) -> str: | ||||
|         t2c = {zs: zli for zs, zli in t2.items() if zs in ("127.0.0.1", "::1")} | ||||
|         t2b = {zs: zli for zs, zli in t2.items() if ":" in zs and zs not in t2c} | ||||
|         t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c} | ||||
|         t2.update(t2b)  # first ipv4, then ipv6... | ||||
|         t2.update(t2c)  # ...and finally localhost | ||||
|  | ||||
|         ip = None | ||||
|         ips = list(t1) + list(t2) | ||||
|         qri = self.args.qri | ||||
| @@ -501,7 +522,7 @@ class TcpSrv(object): | ||||
|                 zoom = 1 | ||||
|  | ||||
|         qr = qrc.render(zoom, pad) | ||||
|         if not VT100: | ||||
|         if self.args.no_ansi: | ||||
|             return "{}\n{}".format(txt, qr) | ||||
|  | ||||
|         halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m" | ||||
|   | ||||
							
								
								
									
										434
									
								
								copyparty/tftpd.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								copyparty/tftpd.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,434 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| try: | ||||
|     from types import SimpleNamespace | ||||
| except: | ||||
|  | ||||
|     class SimpleNamespace(object): | ||||
|         def __init__(self, **attr): | ||||
|             self.__dict__.update(attr) | ||||
|  | ||||
|  | ||||
| import logging | ||||
| import os | ||||
| import re | ||||
| import socket | ||||
| import stat | ||||
| import threading | ||||
| import time | ||||
| from datetime import datetime | ||||
|  | ||||
| try: | ||||
|     import inspect | ||||
| except: | ||||
|     pass | ||||
|  | ||||
| from partftpy import ( | ||||
|     TftpContexts, | ||||
|     TftpPacketFactory, | ||||
|     TftpPacketTypes, | ||||
|     TftpServer, | ||||
|     TftpStates, | ||||
| ) | ||||
| from partftpy.TftpShared import TftpException | ||||
|  | ||||
| from .__init__ import EXE, TYPE_CHECKING | ||||
| from .authsrv import VFS | ||||
| from .bos import bos | ||||
| from .util import BytesIO, Daemon, ODict, exclude_dotfiles, min_ex, runhook, undot | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
|     from typing import Any, Union | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .svchub import SvcHub | ||||
|  | ||||
|  | ||||
| lg = logging.getLogger("tftp") | ||||
| debug, info, warning, error = (lg.debug, lg.info, lg.warning, lg.error) | ||||
|  | ||||
|  | ||||
| def noop(*a, **ka) -> None: | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def _serverInitial(self, pkt: Any, raddress: str, rport: int) -> bool: | ||||
|     info("connection from %s:%s", raddress, rport) | ||||
|     ret = _sinitial[0](self, pkt, raddress, rport) | ||||
|     nm = _hub[0].args.tftp_ipa_nm | ||||
|     if nm and not nm.map(raddress): | ||||
|         yeet("client rejected (--tftp-ipa): %s" % (raddress,)) | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| # patch ipa-check into partftpd (part 1/2) | ||||
| _hub: list["SvcHub"] = [] | ||||
| _sinitial: list[Any] = [] | ||||
|  | ||||
|  | ||||
| class Tftpd(object): | ||||
|     def __init__(self, hub: "SvcHub") -> None: | ||||
|         self.hub = hub | ||||
|         self.args = hub.args | ||||
|         self.asrv = hub.asrv | ||||
|         self.log = hub.log | ||||
|         self.mutex = threading.Lock() | ||||
|  | ||||
|         _hub[:] = [] | ||||
|         _hub.append(hub) | ||||
|  | ||||
|         lg.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) | ||||
|         for x in ["partftpy", "partftpy.TftpStates", "partftpy.TftpServer"]: | ||||
|             lgr = logging.getLogger(x) | ||||
|             lgr.setLevel(logging.DEBUG if self.args.tftpv else logging.INFO) | ||||
|  | ||||
|         if not self.args.tftpv and not self.args.tftpvv: | ||||
|             # contexts -> states -> packettypes -> shared | ||||
|             # contexts -> packetfactory | ||||
|             # packetfactory -> packettypes | ||||
|             Cs = [ | ||||
|                 TftpPacketTypes, | ||||
|                 TftpPacketFactory, | ||||
|                 TftpStates, | ||||
|                 TftpContexts, | ||||
|                 TftpServer, | ||||
|             ] | ||||
|             cbak = [] | ||||
|             if not self.args.tftp_no_fast and not EXE: | ||||
|                 try: | ||||
|                     ptn = re.compile(r"(^\s*)log\.debug\(.*\)$") | ||||
|                     for C in Cs: | ||||
|                         cbak.append(C.__dict__) | ||||
|                         src1 = inspect.getsource(C).split("\n") | ||||
|                         src2 = "\n".join([ptn.sub("\\1pass", ln) for ln in src1]) | ||||
|                         cfn = C.__spec__.origin | ||||
|                         exec (compile(src2, filename=cfn, mode="exec"), C.__dict__) | ||||
|                 except Exception: | ||||
|                     t = "failed to optimize tftp code; run with --tftp-noopt if there are issues:\n" | ||||
|                     self.log("tftp", t + min_ex(), 3) | ||||
|                     for n, zd in enumerate(cbak): | ||||
|                         Cs[n].__dict__ = zd | ||||
|  | ||||
|             for C in Cs: | ||||
|                 C.log.debug = noop | ||||
|  | ||||
|         # patch ipa-check into partftpd (part 2/2) | ||||
|         _sinitial[:] = [] | ||||
|         _sinitial.append(TftpStates.TftpServerState.serverInitial) | ||||
|         TftpStates.TftpServerState.serverInitial = _serverInitial | ||||
|  | ||||
|         # patch vfs into partftpy | ||||
|         TftpContexts.open = self._open | ||||
|         TftpStates.open = self._open | ||||
|  | ||||
|         fos = SimpleNamespace() | ||||
|         for k in os.__dict__: | ||||
|             try: | ||||
|                 setattr(fos, k, getattr(os, k)) | ||||
|             except: | ||||
|                 pass | ||||
|         fos.access = self._access | ||||
|         fos.mkdir = self._mkdir | ||||
|         fos.unlink = self._unlink | ||||
|         fos.sep = "/" | ||||
|         TftpContexts.os = fos | ||||
|         TftpServer.os = fos | ||||
|         TftpStates.os = fos | ||||
|  | ||||
|         fop = SimpleNamespace() | ||||
|         for k in os.path.__dict__: | ||||
|             try: | ||||
|                 setattr(fop, k, getattr(os.path, k)) | ||||
|             except: | ||||
|                 pass | ||||
|         fop.abspath = self._p_abspath | ||||
|         fop.exists = self._p_exists | ||||
|         fop.isdir = self._p_isdir | ||||
|         fop.normpath = self._p_normpath | ||||
|         fos.path = fop | ||||
|  | ||||
|         self._disarm(fos) | ||||
|  | ||||
|         ip = next((x for x in self.args.i if ":" not in x), None) | ||||
|         if not ip: | ||||
|             self.log("tftp", "IPv6 not supported for tftp; listening on 0.0.0.0", 3) | ||||
|             ip = "0.0.0.0" | ||||
|  | ||||
|         self.port = int(self.args.tftp) | ||||
|         self.srv = [] | ||||
|         self.ips = [] | ||||
|  | ||||
|         ports = [] | ||||
|         if self.args.tftp_pr: | ||||
|             p1, p2 = [int(x) for x in self.args.tftp_pr.split("-")] | ||||
|             ports = list(range(p1, p2 + 1)) | ||||
|  | ||||
|         ips = self.args.i | ||||
|         if "::" in ips: | ||||
|             ips.append("0.0.0.0") | ||||
|  | ||||
|         if self.args.ftp4: | ||||
|             ips = [x for x in ips if ":" not in x] | ||||
|  | ||||
|         ips = list(ODict.fromkeys(ips))  # dedup | ||||
|  | ||||
|         for ip in ips: | ||||
|             name = "tftp_%s" % (ip,) | ||||
|             Daemon(self._start, name, [ip, ports]) | ||||
|             time.sleep(0.2)  # give dualstack a chance | ||||
|  | ||||
|     def nlog(self, msg: str, c: Union[int, str] = 0) -> None: | ||||
|         self.log("tftp", msg, c) | ||||
|  | ||||
|     def _start(self, ip, ports): | ||||
|         fam = socket.AF_INET6 if ":" in ip else socket.AF_INET | ||||
|         have_been_alive = False | ||||
|         while True: | ||||
|             srv = TftpServer.TftpServer("/", self._ls) | ||||
|             with self.mutex: | ||||
|                 self.srv.append(srv) | ||||
|                 self.ips.append(ip) | ||||
|  | ||||
|             try: | ||||
|                 # this is the listen loop; it should block forever | ||||
|                 srv.listen(ip, self.port, af_family=fam, ports=ports) | ||||
|             except: | ||||
|                 with self.mutex: | ||||
|                     self.srv.remove(srv) | ||||
|                     self.ips.remove(ip) | ||||
|  | ||||
|                 try: | ||||
|                     srv.sock.close() | ||||
|                 except: | ||||
|                     pass | ||||
|  | ||||
|                 try: | ||||
|                     bound = bool(srv.listenport) | ||||
|                 except: | ||||
|                     bound = False | ||||
|  | ||||
|                 if bound: | ||||
|                     # this instance has managed to bind at least once | ||||
|                     have_been_alive = True | ||||
|  | ||||
|                 if have_been_alive: | ||||
|                     t = "tftp server [%s]:%d crashed; restarting in 3 sec:\n%s" | ||||
|                     error(t, ip, self.port, min_ex()) | ||||
|                     time.sleep(3) | ||||
|                     continue | ||||
|  | ||||
|                 # server failed to start; could be due to dualstack (ipv6 managed to bind and this is ipv4) | ||||
|                 if ip != "0.0.0.0" or "::" not in self.ips: | ||||
|                     # nope, it's fatal | ||||
|                     t = "tftp server [%s]:%d failed to start:\n%s" | ||||
|                     error(t, ip, self.port, min_ex()) | ||||
|  | ||||
|                 # yep; ignore | ||||
|                 # (TODO: move the "listening @ ..." infolog in partftpy to | ||||
|                 #   after the bind attempt so it doesn't print twice) | ||||
|                 return | ||||
|  | ||||
|             info("tftp server [%s]:%d terminated", ip, self.port) | ||||
|             break | ||||
|  | ||||
|     def stop(self): | ||||
|         with self.mutex: | ||||
|             srvs = self.srv[:] | ||||
|  | ||||
|         for srv in srvs: | ||||
|             srv.stop() | ||||
|  | ||||
|     def _v2a(self, caller: str, vpath: str, perms: list, *a: Any) -> tuple[VFS, str]: | ||||
|         vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|         if not perms: | ||||
|             perms = [True, True] | ||||
|  | ||||
|         debug('%s("%s", %s) %s\033[K\033[0m', caller, vpath, str(a), perms) | ||||
|         vfs, rem = self.asrv.vfs.get(vpath, "*", *perms) | ||||
|         return vfs, vfs.canonical(rem) | ||||
|  | ||||
|     def _ls(self, vpath: str, raddress: str, rport: int, force=False) -> Any: | ||||
|         # generate file listing if vpath is dir.txt and return as file object | ||||
|         if not force: | ||||
|             vpath, fn = os.path.split(vpath.replace("\\", "/")) | ||||
|             ptn = self.args.tftp_lsf | ||||
|             if not ptn or not ptn.match(fn.lower()): | ||||
|                 return None | ||||
|  | ||||
|         vn, rem = self.asrv.vfs.get(vpath, "*", True, False) | ||||
|         fsroot, vfs_ls, vfs_virt = vn.ls( | ||||
|             rem, | ||||
|             "*", | ||||
|             not self.args.no_scandir, | ||||
|             [[True, False]], | ||||
|         ) | ||||
|         dnames = set([x[0] for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]) | ||||
|         dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames] | ||||
|         fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames] | ||||
|         real1 = dirs1 + fils1 | ||||
|         realt = [(datetime.fromtimestamp(mt), sz, fn) for mt, sz, fn in real1] | ||||
|         reals = [ | ||||
|             ( | ||||
|                 "%04d-%02d-%02d %02d:%02d:%02d" | ||||
|                 % ( | ||||
|                     zd.year, | ||||
|                     zd.month, | ||||
|                     zd.day, | ||||
|                     zd.hour, | ||||
|                     zd.minute, | ||||
|                     zd.second, | ||||
|                 ), | ||||
|                 sz, | ||||
|                 fn, | ||||
|             ) | ||||
|             for zd, sz, fn in realt | ||||
|         ] | ||||
|         virs = [("????-??-?? ??:??:??", 0, k + "/") for k in vfs_virt.keys()] | ||||
|         ls = virs + reals | ||||
|  | ||||
|         if "*" not in vn.axs.udot: | ||||
|             names = set(exclude_dotfiles([x[2] for x in ls])) | ||||
|             ls = [x for x in ls if x[2] in names] | ||||
|  | ||||
|         try: | ||||
|             biggest = max([x[1] for x in ls]) | ||||
|         except: | ||||
|             biggest = 0 | ||||
|  | ||||
|         perms = [] | ||||
|         if "*" in vn.axs.uread: | ||||
|             perms.append("read") | ||||
|         if "*" in vn.axs.udot: | ||||
|             perms.append("hidden") | ||||
|         if "*" in vn.axs.uwrite: | ||||
|             if "*" in vn.axs.udel: | ||||
|                 perms.append("overwrite") | ||||
|             else: | ||||
|                 perms.append("write") | ||||
|  | ||||
|         fmt = "{{}}  {{:{},}}  {{}}" | ||||
|         fmt = fmt.format(len("{:,}".format(biggest))) | ||||
|         retl = ["# permissions: %s" % (", ".join(perms),)] | ||||
|         retl += [fmt.format(*x) for x in ls] | ||||
|         ret = "\n".join(retl).encode("utf-8", "replace") | ||||
|         return BytesIO(ret + b"\n") | ||||
|  | ||||
|     def _open(self, vpath: str, mode: str, *a: Any, **ka: Any) -> Any: | ||||
|         rd = wr = False | ||||
|         if mode == "rb": | ||||
|             rd = True | ||||
|         elif mode == "wb": | ||||
|             wr = True | ||||
|         else: | ||||
|             raise Exception("bad mode %s" % (mode,)) | ||||
|  | ||||
|         vfs, ap = self._v2a("open", vpath, [rd, wr]) | ||||
|         if wr: | ||||
|             if "*" not in vfs.axs.uwrite: | ||||
|                 yeet("blocked write; folder not world-writable: /%s" % (vpath,)) | ||||
|  | ||||
|             if bos.path.exists(ap) and "*" not in vfs.axs.udel: | ||||
|                 yeet("blocked write; folder not world-deletable: /%s" % (vpath,)) | ||||
|  | ||||
|             xbu = vfs.flags.get("xbu") | ||||
|             if xbu and not runhook( | ||||
|                 self.nlog, xbu, ap, vpath, "", "", 0, 0, "8.3.8.7", 0, "" | ||||
|             ): | ||||
|                 yeet("blocked by xbu server config: " + vpath) | ||||
|  | ||||
|         if not self.args.tftp_nols and bos.path.isdir(ap): | ||||
|             return self._ls(vpath, "", 0, True) | ||||
|  | ||||
|         if not a: | ||||
|             a = [self.args.iobuf] | ||||
|  | ||||
|         return open(ap, mode, *a, **ka) | ||||
|  | ||||
|     def _mkdir(self, vpath: str, *a) -> None: | ||||
|         vfs, ap = self._v2a("mkdir", vpath, []) | ||||
|         if "*" not in vfs.axs.uwrite: | ||||
|             yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,)) | ||||
|  | ||||
|         return bos.mkdir(ap) | ||||
|  | ||||
|     def _unlink(self, vpath: str) -> None: | ||||
|         # return bos.unlink(self._v2a("stat", vpath, *a)[1]) | ||||
|         vfs, ap = self._v2a("delete", vpath, [True, False, False, True]) | ||||
|  | ||||
|         try: | ||||
|             inf = bos.stat(ap) | ||||
|         except: | ||||
|             return | ||||
|  | ||||
|         if not stat.S_ISREG(inf.st_mode) or inf.st_size: | ||||
|             yeet("attempted delete of non-empty file") | ||||
|  | ||||
|         vpath = vpath.replace("\\", "/").lstrip("/") | ||||
|         self.hub.up2k.handle_rm("*", "8.3.8.7", [vpath], [], False, False) | ||||
|  | ||||
|     def _access(self, *a: Any) -> bool: | ||||
|         return True | ||||
|  | ||||
|     def _p_abspath(self, vpath: str) -> str: | ||||
|         return "/" + undot(vpath) | ||||
|  | ||||
|     def _p_normpath(self, *a: Any) -> str: | ||||
|         return "" | ||||
|  | ||||
|     def _p_exists(self, vpath: str) -> bool: | ||||
|         try: | ||||
|             ap = self._v2a("p.exists", vpath, [False, False])[1] | ||||
|             bos.stat(ap) | ||||
|             return True | ||||
|         except: | ||||
|             return False | ||||
|  | ||||
|     def _p_isdir(self, vpath: str) -> bool: | ||||
|         try: | ||||
|             st = bos.stat(self._v2a("p.isdir", vpath, [False, False])[1]) | ||||
|             ret = stat.S_ISDIR(st.st_mode) | ||||
|             return ret | ||||
|         except: | ||||
|             return False | ||||
|  | ||||
|     def _hook(self, *a: Any, **ka: Any) -> None: | ||||
|         src = inspect.currentframe().f_back.f_code.co_name | ||||
|         error("\033[31m%s:hook(%s)\033[0m", src, a) | ||||
|         raise Exception("nope") | ||||
|  | ||||
|     def _disarm(self, fos: SimpleNamespace) -> None: | ||||
|         fos.chmod = self._hook | ||||
|         fos.chown = self._hook | ||||
|         fos.close = self._hook | ||||
|         fos.ftruncate = self._hook | ||||
|         fos.lchown = self._hook | ||||
|         fos.link = self._hook | ||||
|         fos.listdir = self._hook | ||||
|         fos.lstat = self._hook | ||||
|         fos.open = self._hook | ||||
|         fos.remove = self._hook | ||||
|         fos.rename = self._hook | ||||
|         fos.replace = self._hook | ||||
|         fos.scandir = self._hook | ||||
|         fos.stat = self._hook | ||||
|         fos.symlink = self._hook | ||||
|         fos.truncate = self._hook | ||||
|         fos.utime = self._hook | ||||
|         fos.walk = self._hook | ||||
|  | ||||
|         fos.path.expanduser = self._hook | ||||
|         fos.path.expandvars = self._hook | ||||
|         fos.path.getatime = self._hook | ||||
|         fos.path.getctime = self._hook | ||||
|         fos.path.getmtime = self._hook | ||||
|         fos.path.getsize = self._hook | ||||
|         fos.path.isabs = self._hook | ||||
|         fos.path.isfile = self._hook | ||||
|         fos.path.islink = self._hook | ||||
|         fos.path.realpath = self._hook | ||||
|  | ||||
|  | ||||
| def yeet(msg: str) -> None: | ||||
|     warning(msg) | ||||
|     raise TftpException(msg) | ||||
| @@ -31,7 +31,7 @@ class ThumbCli(object): | ||||
|             if not c: | ||||
|                 raise Exception() | ||||
|         except: | ||||
|             c = {k: {} for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]} | ||||
|             c = {k: set() for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]} | ||||
|  | ||||
|         self.thumbable = c["thumbable"] | ||||
|         self.fmt_pil = c["pil"] | ||||
| @@ -57,7 +57,7 @@ class ThumbCli(object): | ||||
|         if is_vid and "dvthumb" in dbv.flags: | ||||
|             return None | ||||
|  | ||||
|         want_opus = fmt in ("opus", "caf") | ||||
|         want_opus = fmt in ("opus", "caf", "mp3") | ||||
|         is_au = ext in self.fmt_ffa | ||||
|         if is_au: | ||||
|             if want_opus: | ||||
| @@ -78,23 +78,46 @@ class ThumbCli(object): | ||||
|         if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg", "png"]: | ||||
|             return os.path.join(ptop, rem) | ||||
|  | ||||
|         if fmt == "j" and self.args.th_no_jpg: | ||||
|             fmt = "w" | ||||
|         if fmt[:1] in "jw": | ||||
|             sfmt = fmt[:1] | ||||
|  | ||||
|         if fmt == "w": | ||||
|             if ( | ||||
|                 self.args.th_no_webp | ||||
|                 or (is_img and not self.can_webp) | ||||
|                 or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) | ||||
|             ): | ||||
|                 fmt = "j" | ||||
|             if sfmt == "j" and self.args.th_no_jpg: | ||||
|                 sfmt = "w" | ||||
|  | ||||
|             if sfmt == "w": | ||||
|                 if ( | ||||
|                     self.args.th_no_webp | ||||
|                     or (is_img and not self.can_webp) | ||||
|                     or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) | ||||
|                 ): | ||||
|                     sfmt = "j" | ||||
|  | ||||
|             vf_crop = dbv.flags["crop"] | ||||
|             vf_th3x = dbv.flags["th3x"] | ||||
|  | ||||
|             if "f" in vf_crop: | ||||
|                 sfmt += "f" if "n" in vf_crop else "" | ||||
|             else: | ||||
|                 sfmt += "f" if "f" in fmt else "" | ||||
|  | ||||
|             if "f" in vf_th3x: | ||||
|                 sfmt += "3" if "y" in vf_th3x else "" | ||||
|             else: | ||||
|                 sfmt += "3" if "3" in fmt else "" | ||||
|  | ||||
|             fmt = sfmt | ||||
|          | ||||
|         elif fmt[:1] == "p" and not is_au: | ||||
|             t = "cannot thumbnail [%s]: png only allowed for waveforms" | ||||
|             self.log(t % (rem), 6) | ||||
|             return None | ||||
|  | ||||
|         histpath = self.asrv.vfs.histtab.get(ptop) | ||||
|         if not histpath: | ||||
|             self.log("no histpath for [{}]".format(ptop)) | ||||
|             return None | ||||
|  | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt) | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) | ||||
|         tpaths = [tpath] | ||||
|         if fmt == "w": | ||||
|             # also check for jpg (maybe webp is unavailable) | ||||
| @@ -108,6 +131,7 @@ class ThumbCli(object): | ||||
|                 if st.st_size: | ||||
|                     ret = tpath = tp | ||||
|                     fmt = ret.rsplit(".")[1] | ||||
|                     break | ||||
|                 else: | ||||
|                     abort = True | ||||
|             except: | ||||
|   | ||||
| @@ -12,19 +12,24 @@ import time | ||||
|  | ||||
| from queue import Queue | ||||
|  | ||||
| from .__init__ import TYPE_CHECKING | ||||
| from .__init__ import ANYWIN, TYPE_CHECKING | ||||
| from .authsrv import VFS | ||||
| from .bos import bos | ||||
| from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe | ||||
| from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe | ||||
| from .util import BytesIO  # type: ignore | ||||
| from .util import ( | ||||
|     BytesIO, | ||||
|     FFMPEG_URL, | ||||
|     Cooldown, | ||||
|     Daemon, | ||||
|     Pebkac, | ||||
|     afsenc, | ||||
|     fsenc, | ||||
|     min_ex, | ||||
|     runcmd, | ||||
|     statdir, | ||||
|     vsplit, | ||||
|     wrename, | ||||
|     wunlink, | ||||
| ) | ||||
|  | ||||
| if True:  # pylint: disable=using-constant-test | ||||
| @@ -34,14 +39,21 @@ if TYPE_CHECKING: | ||||
|     from .svchub import SvcHub | ||||
|  | ||||
| HAVE_PIL = False | ||||
| HAVE_PILF = False | ||||
| HAVE_HEIF = False | ||||
| HAVE_AVIF = False | ||||
| HAVE_WEBP = False | ||||
|  | ||||
| try: | ||||
|     from PIL import ExifTags, Image, ImageOps | ||||
|     from PIL import ExifTags, Image, ImageFont, ImageOps | ||||
|  | ||||
|     HAVE_PIL = True | ||||
|     try: | ||||
|         ImageFont.load_default(size=16) | ||||
|         HAVE_PILF = True | ||||
|     except: | ||||
|         pass | ||||
|  | ||||
|     try: | ||||
|         Image.new("RGB", (2, 2)).save(BytesIO(), format="webp") | ||||
|         HAVE_WEBP = True | ||||
| @@ -76,29 +88,36 @@ except: | ||||
|     HAVE_VIPS = False | ||||
|  | ||||
|  | ||||
| def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str: | ||||
| def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) -> str: | ||||
|     # base16 = 16 = 256 | ||||
|     # b64-lc = 38 = 1444 | ||||
|     # base64 = 64 = 4096 | ||||
|     rd, fn = vsplit(rem) | ||||
|     if rd: | ||||
|         h = hashlib.sha512(fsenc(rd)).digest() | ||||
|         b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24] | ||||
|         rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64 | ||||
|     else: | ||||
|         rd = "top" | ||||
|     if not rd: | ||||
|         rd = "\ntop" | ||||
|  | ||||
|     # spectrograms are never cropped; strip fullsize flag | ||||
|     ext = rem.split(".")[-1].lower() | ||||
|     if ext in ffa and fmt[:2] in ("wf", "jf"): | ||||
|         fmt = fmt.replace("f", "") | ||||
|  | ||||
|     rd += "\n" + fmt | ||||
|     h = hashlib.sha512(afsenc(rd)).digest() | ||||
|     b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24] | ||||
|     rd = ("%s/%s/" % (b64[:2], b64[2:4])).lower() + b64 | ||||
|  | ||||
|     # could keep original filenames but this is safer re pathlen | ||||
|     h = hashlib.sha512(fsenc(fn)).digest() | ||||
|     h = hashlib.sha512(afsenc(fn)).digest() | ||||
|     fn = base64.urlsafe_b64encode(h).decode("ascii")[:24] | ||||
|  | ||||
|     if fmt in ("opus", "caf"): | ||||
|     if fmt in ("opus", "caf", "mp3"): | ||||
|         cat = "ac" | ||||
|     else: | ||||
|         fmt = "webp" if fmt == "w" else "png" if fmt == "p" else "jpg" | ||||
|         fc = fmt[:1] | ||||
|         fmt = "webp" if fc == "w" else "png" if fc == "p" else "jpg" | ||||
|         cat = "th" | ||||
|  | ||||
|     return "{}/{}/{}/{}.{:x}.{}".format(histpath, cat, rd, fn, int(mtime), fmt) | ||||
|     return "%s/%s/%s/%s.%x.%s" % (histpath, cat, rd, fn, int(mtime), fmt) | ||||
|  | ||||
|  | ||||
| class ThumbSrv(object): | ||||
| @@ -108,16 +127,16 @@ class ThumbSrv(object): | ||||
|         self.args = hub.args | ||||
|         self.log_func = hub.log | ||||
|  | ||||
|         res = hub.args.th_size.split("x") | ||||
|         self.res = tuple([int(x) for x in res]) | ||||
|         self.poke_cd = Cooldown(self.args.th_poke) | ||||
|  | ||||
|         self.mutex = threading.Lock() | ||||
|         self.busy: dict[str, list[threading.Condition]] = {} | ||||
|         self.ram: dict[str, float] = {} | ||||
|         self.memcond = threading.Condition(self.mutex) | ||||
|         self.stopping = False | ||||
|         self.nthr = max(1, self.args.th_mt) | ||||
|  | ||||
|         self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4) | ||||
|         self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4) | ||||
|         for n in range(self.nthr): | ||||
|             Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr)) | ||||
|  | ||||
| @@ -133,6 +152,8 @@ class ThumbSrv(object): | ||||
|             msg = "cannot create audio/video thumbnails because some of the required programs are not available: " | ||||
|             msg += ", ".join(missing) | ||||
|             self.log(msg, c=3) | ||||
|             if ANYWIN and self.args.no_acode: | ||||
|                 self.log("download FFmpeg to fix it:\033[0m " + FFMPEG_URL, 3) | ||||
|  | ||||
|         if self.args.th_clean: | ||||
|             Daemon(self.cleaner, "thumb.cln") | ||||
| @@ -180,35 +201,46 @@ class ThumbSrv(object): | ||||
|         with self.mutex: | ||||
|             return not self.nthr | ||||
|  | ||||
|     def getres(self, vn: VFS, fmt: str) -> tuple[int, int]: | ||||
|         mul = 3 if "3" in fmt else 1 | ||||
|         w, h = vn.flags["thsize"].split("x") | ||||
|         return int(w) * mul, int(h) * mul | ||||
|  | ||||
|     def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]: | ||||
|         histpath = self.asrv.vfs.histtab.get(ptop) | ||||
|         if not histpath: | ||||
|             self.log("no histpath for [{}]".format(ptop)) | ||||
|             return None | ||||
|  | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt) | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa) | ||||
|         abspath = os.path.join(ptop, rem) | ||||
|         cond = threading.Condition(self.mutex) | ||||
|         do_conv = False | ||||
|         with self.mutex: | ||||
|             try: | ||||
|                 self.busy[tpath].append(cond) | ||||
|                 self.log("wait {}".format(tpath)) | ||||
|                 self.log("joined waiting room for %s" % (tpath,)) | ||||
|             except: | ||||
|                 thdir = os.path.dirname(tpath) | ||||
|                 bos.makedirs(thdir) | ||||
|                 bos.makedirs(os.path.join(thdir, "w")) | ||||
|  | ||||
|                 inf_path = os.path.join(thdir, "dir.txt") | ||||
|                 if not bos.path.exists(inf_path): | ||||
|                     with open(inf_path, "wb") as f: | ||||
|                         f.write(fsenc(os.path.dirname(abspath))) | ||||
|                         f.write(afsenc(os.path.dirname(abspath))) | ||||
|  | ||||
|                 self.busy[tpath] = [cond] | ||||
|                 do_conv = True | ||||
|  | ||||
|         if do_conv: | ||||
|             self.q.put((abspath, tpath)) | ||||
|             self.log("conv {} \033[0m{}".format(tpath, abspath), c=6) | ||||
|             allvols = list(self.asrv.vfs.all_vols.values()) | ||||
|             vn = next((x for x in allvols if x.realpath == ptop), None) | ||||
|             if not vn: | ||||
|                 self.log("ptop [{}] not in {}".format(ptop, allvols), 3) | ||||
|                 vn = self.asrv.vfs.all_aps[0][1] | ||||
|  | ||||
|             self.q.put((abspath, tpath, fmt, vn)) | ||||
|             self.log("conv {} :{} \033[0m{}".format(tpath, fmt, abspath), c=6) | ||||
|  | ||||
|         while not self.stopping: | ||||
|             with self.mutex: | ||||
| @@ -238,16 +270,39 @@ class ThumbSrv(object): | ||||
|             "ffa": self.fmt_ffa, | ||||
|         } | ||||
|  | ||||
|     def wait4ram(self, need: float, ttpath: str) -> None: | ||||
|         ram = self.args.th_ram_max | ||||
|         if need > ram * 0.99: | ||||
|             t = "file too big; need %.2f GiB RAM, but --th-ram-max is only %.1f" | ||||
|             raise Exception(t % (need, ram)) | ||||
|  | ||||
|         while True: | ||||
|             with self.mutex: | ||||
|                 used = sum([v for k, v in self.ram.items() if k != ttpath]) + need | ||||
|                 if used < ram: | ||||
|                     # self.log("XXX self.ram: %s" % (self.ram,), 5) | ||||
|                     self.ram[ttpath] = need | ||||
|                     return | ||||
|             with self.memcond: | ||||
|                 # self.log("at RAM limit; used %.2f GiB, need %.2f more" % (used-need, need), 1) | ||||
|                 self.memcond.wait(3) | ||||
|  | ||||
|     def worker(self) -> None: | ||||
|         while not self.stopping: | ||||
|             task = self.q.get() | ||||
|             if not task: | ||||
|                 break | ||||
|  | ||||
|             abspath, tpath = task | ||||
|             abspath, tpath, fmt, vn = task | ||||
|             ext = abspath.split(".")[-1].lower() | ||||
|             png_ok = False | ||||
|             funs = [] | ||||
|  | ||||
|             if ext in self.args.au_unpk: | ||||
|                 ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn) | ||||
|             else: | ||||
|                 ap_unpk = abspath | ||||
|  | ||||
|             if not bos.path.exists(tpath): | ||||
|                 for lib in self.args.th_dec: | ||||
|                     if lib == "pil" and ext in self.fmt_pil: | ||||
| @@ -259,18 +314,27 @@ class ThumbSrv(object): | ||||
|                     elif lib == "ff" and ext in self.fmt_ffa: | ||||
|                         if tpath.endswith(".opus") or tpath.endswith(".caf"): | ||||
|                             funs.append(self.conv_opus) | ||||
|                         elif tpath.endswith(".mp3"): | ||||
|                             funs.append(self.conv_mp3) | ||||
|                         elif tpath.endswith(".png"): | ||||
|                             funs.append(self.conv_waves) | ||||
|                             png_ok = True | ||||
|                         else: | ||||
|                             funs.append(self.conv_spec) | ||||
|  | ||||
|             if not png_ok and tpath.endswith(".png"): | ||||
|                 raise Pebkac(400, "png only allowed for waveforms") | ||||
|             tdir, tfn = os.path.split(tpath) | ||||
|             ttpath = os.path.join(tdir, "w", tfn) | ||||
|             try: | ||||
|                 wunlink(self.log, ttpath, vn.flags) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|             for fun in funs: | ||||
|                 try: | ||||
|                     fun(abspath, tpath) | ||||
|                     if not png_ok and tpath.endswith(".png"): | ||||
|                         raise Exception("png only allowed for waveforms") | ||||
|  | ||||
|                     fun(ap_unpk, ttpath, fmt, vn) | ||||
|                     break | ||||
|                 except Exception as ex: | ||||
|                     msg = "{} could not create thumbnail of {}\n{}" | ||||
| @@ -279,29 +343,42 @@ class ThumbSrv(object): | ||||
|                     self.log(msg, c) | ||||
|                     if getattr(ex, "returncode", 0) != 321: | ||||
|                         if fun == funs[-1]: | ||||
|                             with open(tpath, "wb") as _: | ||||
|                             with open(ttpath, "wb") as _: | ||||
|                                 pass | ||||
|                     else: | ||||
|                         # ffmpeg may spawn empty files on windows | ||||
|                         try: | ||||
|                             os.unlink(tpath) | ||||
|                             wunlink(self.log, ttpath, vn.flags) | ||||
|                         except: | ||||
|                             pass | ||||
|  | ||||
|             if abspath != ap_unpk: | ||||
|                 wunlink(self.log, ap_unpk, vn.flags) | ||||
|  | ||||
|             try: | ||||
|                 wrename(self.log, ttpath, tpath, vn.flags) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|             with self.mutex: | ||||
|                 subs = self.busy[tpath] | ||||
|                 del self.busy[tpath] | ||||
|                 self.ram.pop(ttpath, None) | ||||
|  | ||||
|             for x in subs: | ||||
|                 with x: | ||||
|                     x.notify_all() | ||||
|  | ||||
|             with self.memcond: | ||||
|                 self.memcond.notify_all() | ||||
|  | ||||
|         with self.mutex: | ||||
|             self.nthr -= 1 | ||||
|  | ||||
|     def fancy_pillow(self, im: "Image.Image") -> "Image.Image": | ||||
|     def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image": | ||||
|         # exif_transpose is expensive (loads full image + unconditional copy) | ||||
|         r = max(*self.res) * 2 | ||||
|         res = self.getres(vn, fmt) | ||||
|         r = max(*res) * 2 | ||||
|         im.thumbnail((r, r), resample=Image.LANCZOS) | ||||
|         try: | ||||
|             k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation") | ||||
| @@ -315,23 +392,24 @@ class ThumbSrv(object): | ||||
|         if rot in rots: | ||||
|             im = im.transpose(rots[rot]) | ||||
|  | ||||
|         if self.args.th_no_crop: | ||||
|             im.thumbnail(self.res, resample=Image.LANCZOS) | ||||
|         if "f" in fmt: | ||||
|             im.thumbnail(res, resample=Image.LANCZOS) | ||||
|         else: | ||||
|             iw, ih = im.size | ||||
|             dw, dh = self.res | ||||
|             dw, dh = res | ||||
|             res = (min(iw, dw), min(ih, dh)) | ||||
|             im = ImageOps.fit(im, res, method=Image.LANCZOS) | ||||
|  | ||||
|         return im | ||||
|  | ||||
|     def conv_pil(self, abspath: str, tpath: str) -> None: | ||||
|     def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         self.wait4ram(0.2, tpath) | ||||
|         with Image.open(fsenc(abspath)) as im: | ||||
|             try: | ||||
|                 im = self.fancy_pillow(im) | ||||
|                 im = self.fancy_pillow(im, fmt, vn) | ||||
|             except Exception as ex: | ||||
|                 self.log("fancy_pillow {}".format(ex), "90") | ||||
|                 im.thumbnail(self.res) | ||||
|                 im.thumbnail(self.getres(vn, fmt)) | ||||
|  | ||||
|             fmts = ["RGB", "L"] | ||||
|             args = {"quality": 40} | ||||
| @@ -342,7 +420,7 @@ class ThumbSrv(object): | ||||
|                 # method 0 = pillow-default, fast | ||||
|                 # method 4 = ffmpeg-default | ||||
|                 # method 6 = max, slow | ||||
|                 fmts += ["RGBA", "LA"] | ||||
|                 fmts.extend(("RGBA", "LA")) | ||||
|                 args["method"] = 6 | ||||
|             else: | ||||
|                 # default q = 75 | ||||
| @@ -354,12 +432,13 @@ class ThumbSrv(object): | ||||
|  | ||||
|             im.save(tpath, **args) | ||||
|  | ||||
|     def conv_vips(self, abspath: str, tpath: str) -> None: | ||||
|     def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         self.wait4ram(0.2, tpath) | ||||
|         crops = ["centre", "none"] | ||||
|         if self.args.th_no_crop: | ||||
|         if "f" in fmt: | ||||
|             crops = ["none"] | ||||
|  | ||||
|         w, h = self.res | ||||
|         w, h = self.getres(vn, fmt) | ||||
|         kw = {"height": h, "size": "down", "intent": "relative"} | ||||
|  | ||||
|         for c in crops: | ||||
| @@ -371,10 +450,12 @@ class ThumbSrv(object): | ||||
|                 if c == crops[-1]: | ||||
|                     raise | ||||
|  | ||||
|         assert img  # type: ignore | ||||
|         img.write_to_file(tpath, Q=40) | ||||
|  | ||||
|     def conv_ffmpeg(self, abspath: str, tpath: str) -> None: | ||||
|         ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) | ||||
|     def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         self.wait4ram(0.2, tpath) | ||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||
|         if not ret: | ||||
|             return | ||||
|  | ||||
| @@ -386,12 +467,13 @@ class ThumbSrv(object): | ||||
|             seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] | ||||
|  | ||||
|         scale = "scale={0}:{1}:force_original_aspect_ratio=" | ||||
|         if self.args.th_no_crop: | ||||
|         if "f" in fmt: | ||||
|             scale += "decrease,setsar=1:1" | ||||
|         else: | ||||
|             scale += "increase,crop={0}:{1},setsar=1:1" | ||||
|  | ||||
|         bscale = scale.format(*list(self.res)).encode("utf-8") | ||||
|         res = self.getres(vn, fmt) | ||||
|         bscale = scale.format(*list(res)).encode("utf-8") | ||||
|         # fmt: off | ||||
|         cmd = [ | ||||
|             b"ffmpeg", | ||||
| @@ -423,11 +505,11 @@ class ThumbSrv(object): | ||||
|             ] | ||||
|  | ||||
|         cmd += [fsenc(tpath)] | ||||
|         self._run_ff(cmd) | ||||
|         self._run_ff(cmd, vn) | ||||
|  | ||||
|     def _run_ff(self, cmd: list[bytes]) -> None: | ||||
|     def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None: | ||||
|         # self.log((b" ".join(cmd)).decode("utf-8")) | ||||
|         ret, _, serr = runcmd(cmd, timeout=self.args.th_convt) | ||||
|         ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom) | ||||
|         if not ret: | ||||
|             return | ||||
|  | ||||
| @@ -470,13 +552,26 @@ class ThumbSrv(object): | ||||
|         self.log(t + txt, c=c) | ||||
|         raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) | ||||
|  | ||||
|     def conv_waves(self, abspath: str, tpath: str) -> None: | ||||
|         ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) | ||||
|     def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||
|         if "ac" not in ret: | ||||
|             raise Exception("not audio") | ||||
|  | ||||
|         flt = ( | ||||
|             b"[0:a:0]" | ||||
|         # jt_versi.xm: 405M/839s | ||||
|         dur = ret[".dur"][1] if ".dur" in ret else 300 | ||||
|         need = 0.2 + dur / 3000 | ||||
|         speedup = b"" | ||||
|         if need > self.args.th_ram_max * 0.7: | ||||
|             self.log("waves too big (need %.2f GiB); trying to optimize" % (need,)) | ||||
|             need = 0.2 + dur / 4200  # only helps about this much... | ||||
|             speedup = b"aresample=8000," | ||||
|         if need > self.args.th_ram_max * 0.96: | ||||
|             raise Exception("file too big; cannot waves") | ||||
|  | ||||
|         self.wait4ram(need, tpath) | ||||
|  | ||||
|         flt = b"[0:a:0]" + speedup | ||||
|         flt += ( | ||||
|             b"compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2" | ||||
|             b",volume=2" | ||||
|             b",showwavespic=s=2048x64:colors=white" | ||||
| @@ -496,14 +591,45 @@ class ThumbSrv(object): | ||||
|         # fmt: on | ||||
|  | ||||
|         cmd += [fsenc(tpath)] | ||||
|         self._run_ff(cmd) | ||||
|         self._run_ff(cmd, vn) | ||||
|  | ||||
|     def conv_spec(self, abspath: str, tpath: str) -> None: | ||||
|         ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) | ||||
|         if "pngquant" in vn.flags: | ||||
|             wtpath = tpath + ".png" | ||||
|             cmd = [ | ||||
|                 b"pngquant", | ||||
|                 b"--strip", | ||||
|                 b"--nofs", | ||||
|                 b"--output", fsenc(wtpath), | ||||
|                 fsenc(tpath) | ||||
|             ] | ||||
|             ret = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=400)[0] | ||||
|             if ret: | ||||
|                 try: | ||||
|                     wunlink(self.log, wtpath,  vn.flags) | ||||
|                 except: | ||||
|                     pass | ||||
|             else: | ||||
|                 wrename(self.log, wtpath, tpath, vn.flags) | ||||
|  | ||||
|     def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||
|         if "ac" not in ret: | ||||
|             raise Exception("not audio") | ||||
|  | ||||
|         fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]" | ||||
|         # https://trac.ffmpeg.org/ticket/10797 | ||||
|         # expect 1 GiB every 600 seconds when duration is tricky; | ||||
|         # simple filetypes are generally safer so let's special-case those | ||||
|         safe = ("flac", "wav", "aif", "aiff", "opus") | ||||
|         coeff = 1800 if abspath.split(".")[-1].lower() in safe else 600 | ||||
|         dur = ret[".dur"][1] if ".dur" in ret else 300 | ||||
|         need = 0.2 + dur / coeff | ||||
|         self.wait4ram(need, tpath) | ||||
|  | ||||
|         fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" | ||||
|         if "3" in fmt: | ||||
|             fc += "1280x1024,crop=1420:1056:70:48[o]" | ||||
|         else: | ||||
|             fc += "640x512,crop=780:544:70:48[o]" | ||||
|  | ||||
|         if self.args.th_ff_swr: | ||||
|             fco = ":filter_size=128:cutoff=0.877" | ||||
| @@ -539,23 +665,75 @@ class ThumbSrv(object): | ||||
|             ] | ||||
|  | ||||
|         cmd += [fsenc(tpath)] | ||||
|         self._run_ff(cmd) | ||||
|         self._run_ff(cmd, vn) | ||||
|  | ||||
|     def conv_opus(self, abspath: str, tpath: str) -> None: | ||||
|         if self.args.no_acode: | ||||
|     def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         quality = self.args.q_mp3.lower() | ||||
|         if self.args.no_acode or not quality: | ||||
|             raise Exception("disabled in server config") | ||||
|  | ||||
|         ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) | ||||
|         self.wait4ram(0.2, tpath) | ||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||
|         if "ac" not in ret: | ||||
|             raise Exception("not audio") | ||||
|  | ||||
|         if quality.endswith("k"): | ||||
|             qk = b"-b:a" | ||||
|             qv = quality.encode("ascii") | ||||
|         else: | ||||
|             qk = b"-q:a" | ||||
|             qv = quality[1:].encode("ascii") | ||||
|  | ||||
|         # extremely conservative choices for output format | ||||
|         # (always 2ch 44k1) because if a device is old enough | ||||
|         # to not support opus then it's probably also super picky | ||||
|  | ||||
|         # fmt: off | ||||
|         cmd = [ | ||||
|             b"ffmpeg", | ||||
|             b"-nostdin", | ||||
|             b"-v", b"error", | ||||
|             b"-hide_banner", | ||||
|             b"-i", fsenc(abspath), | ||||
|             b"-map_metadata", b"-1", | ||||
|             b"-map", b"0:a:0", | ||||
|             b"-ar", b"44100", | ||||
|             b"-ac", b"2", | ||||
|             b"-c:a", b"libmp3lame", | ||||
|             qk, qv, | ||||
|             fsenc(tpath) | ||||
|         ] | ||||
|         # fmt: on | ||||
|         self._run_ff(cmd, vn, oom=300) | ||||
|  | ||||
|     def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: | ||||
|         if self.args.no_acode or not self.args.q_opus: | ||||
|             raise Exception("disabled in server config") | ||||
|  | ||||
|         self.wait4ram(0.2, tpath) | ||||
|         ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) | ||||
|         if "ac" not in ret: | ||||
|             raise Exception("not audio") | ||||
|  | ||||
|         try: | ||||
|             dur = ret[".dur"][1] | ||||
|         except: | ||||
|             dur = 0 | ||||
|  | ||||
|         src_opus = abspath.lower().endswith(".opus") or ret["ac"][1] == "opus" | ||||
|         want_caf = tpath.endswith(".caf") | ||||
|         tmp_opus = tpath | ||||
|         if want_caf: | ||||
|             tmp_opus = tpath.rsplit(".", 1)[0] + ".opus" | ||||
|             tmp_opus = tpath + ".opus" | ||||
|             try: | ||||
|                 wunlink(self.log, tmp_opus, vn.flags) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|         if not want_caf or (not src_opus and not bos.path.isfile(tmp_opus)): | ||||
|         caf_src = abspath if src_opus else tmp_opus | ||||
|         bq = ("%dk" % (self.args.q_opus,)).encode("ascii") | ||||
|  | ||||
|         if not want_caf or not src_opus: | ||||
|             # fmt: off | ||||
|             cmd = [ | ||||
|                 b"ffmpeg", | ||||
| @@ -566,13 +744,38 @@ class ThumbSrv(object): | ||||
|                 b"-map_metadata", b"-1", | ||||
|                 b"-map", b"0:a:0", | ||||
|                 b"-c:a", b"libopus", | ||||
|                 b"-b:a", b"128k", | ||||
|                 b"-b:a", bq, | ||||
|                 fsenc(tmp_opus) | ||||
|             ] | ||||
|             # fmt: on | ||||
|             self._run_ff(cmd) | ||||
|             self._run_ff(cmd, vn, oom=300) | ||||
|  | ||||
|         if want_caf: | ||||
|         # iOS fails to play some "insufficiently complex" files | ||||
|         # (average file shorter than 8 seconds), so of course we | ||||
|         # fix that by mixing in some inaudible pink noise :^) | ||||
|         # 6.3 sec seems like the cutoff so lets do 7, and | ||||
|         # 7 sec of psyqui-musou.opus @ 3:50 is 174 KiB | ||||
|         if want_caf and (dur < 20 or bos.path.getsize(caf_src) < 256 * 1024): | ||||
|             # fmt: off | ||||
|             cmd = [ | ||||
|                 b"ffmpeg", | ||||
|                 b"-nostdin", | ||||
|                 b"-v", b"error", | ||||
|                 b"-hide_banner", | ||||
|                 b"-i", fsenc(abspath), | ||||
|                 b"-filter_complex", b"anoisesrc=a=0.001:d=7:c=pink,asplit[l][r]; [l][r]amerge[s]; [0:a:0][s]amix", | ||||
|                 b"-map_metadata", b"-1", | ||||
|                 b"-ac", b"2", | ||||
|                 b"-c:a", b"libopus", | ||||
|                 b"-b:a", bq, | ||||
|                 b"-f", b"caf", | ||||
|                 fsenc(tpath) | ||||
|             ] | ||||
|             # fmt: on | ||||
|             self._run_ff(cmd, vn, oom=300) | ||||
|  | ||||
|         elif want_caf: | ||||
|             # simple remux should be safe | ||||
|             # fmt: off | ||||
|             cmd = [ | ||||
|                 b"ffmpeg", | ||||
| @@ -587,7 +790,13 @@ class ThumbSrv(object): | ||||
|                 fsenc(tpath) | ||||
|             ] | ||||
|             # fmt: on | ||||
|             self._run_ff(cmd) | ||||
|             self._run_ff(cmd, vn, oom=300) | ||||
|  | ||||
|         if tmp_opus != tpath: | ||||
|             try: | ||||
|                 wunlink(self.log, tmp_opus, vn.flags) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|     def poke(self, tdir: str) -> None: | ||||
|         if not self.poke_cd.poke(tdir): | ||||
| @@ -612,7 +821,10 @@ class ThumbSrv(object): | ||||
|                 else: | ||||
|                     self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol)) | ||||
|  | ||||
|                 ndirs += self.clean(histpath) | ||||
|                 try: | ||||
|                     ndirs += self.clean(histpath) | ||||
|                 except Exception as ex: | ||||
|                     self.log("\033[Jcln err in %s: %r" % (histpath, ex), 3) | ||||
|  | ||||
|             self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) | ||||
|  | ||||
| @@ -629,7 +841,7 @@ class ThumbSrv(object): | ||||
|  | ||||
|     def _clean(self, cat: str, thumbpath: str) -> int: | ||||
|         # self.log("cln {}".format(thumbpath)) | ||||
|         exts = ["jpg", "webp", "png"] if cat == "th" else ["opus", "caf"] | ||||
|         exts = ["jpg", "webp", "png"] if cat == "th" else ["opus", "caf", "mp3"] | ||||
|         maxage = getattr(self.args, cat + "_maxage") | ||||
|         now = time.time() | ||||
|         prev_b64 = None | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user