mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-31 04:30:44 +00:00
Compare commits
913 Commits
v1.3.0
...
93d9d509d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d9d509d5 | |||
| 98ed26b470 | |||
| 0babc6b4ab | |||
| 55d2549e1f | |||
| f3e3ff1432 | |||
| aa3861af9c | |||
| 34660d3615 | |||
| d63377a851 | |||
| a20eb6866a | |||
| 809ab47042 | |||
| 0b84009b4a | |||
| 92d7561d49 | |||
| e908531bef | |||
| f8620d10b2 | |||
| 8be72b48a1 | |||
| 5655480e00 | |||
| 3c8defa304 | |||
| 1ed50697ec | |||
| 3b432d42b5 | |||
| 89e3bdfa07 | |||
| f8d2d3cb8d | |||
| 01004e19c0 | |||
| 92bb28149c | |||
| 652210acfa | |||
| 45f3da256b | |||
| 3c5e83366a | |||
| a7a4bb7001 | |||
| 2f7f726d4c | |||
| 43d7284657 | |||
| 02423af931 | |||
| 2a228625e2 | |||
| 16e0302f30 | |||
| 6ecbc9ba90 | |||
| 7276b9621f | |||
| fb7734b5b0 | |||
| 29c37293b9 | |||
| f25ba2e6b0 | |||
| 6a5b9ddacf | |||
| 48355f65ed | |||
| d616977904 | |||
| 3c068febde | |||
| 76adbe49e0 | |||
| f6221a2413 | |||
| 4f46cb04f5 | |||
| 66e814c086 | |||
| b3f5d96490 | |||
| 144265122a | |||
| 4f95d75c37 | |||
| 8db1d2e7f1 | |||
| 4088a811cd | |||
| 2ac4c75fad | |||
| e2ebcbb02a | |||
| 9defe70190 | |||
| 23943d47e9 | |||
| 49f342878a | |||
| 1f29c65a95 | |||
| a65f79eb79 | |||
| c6a9f82358 | |||
| 1702537979 | |||
| 12604b9379 | |||
| 9b523e6bd5 | |||
| aa030e3f36 | |||
| 8e739524b0 | |||
| 4ba4c0cba6 | |||
| 48d02918c1 | |||
| d672bfa317 | |||
| 9a90038dd2 | |||
| 7d23196f76 | |||
| 2c6812934e | |||
| 60f3eb151b | |||
| 0db9797c4e | |||
| a2ffffedc1 | |||
| 19b98c62c1 | |||
| 06fea598e1 | |||
| 9a6f06d23e | |||
| 67f6a0e458 | |||
| a57e2aecd7 | |||
| 1e7c15b69b | |||
| a70f572efd | |||
| 8c183b45c6 | |||
| a8f7f58dab | |||
| 31fe26de5e | |||
| eb1b770dc2 | |||
| abb2ad635f | |||
| a75df0a697 | |||
| ea797f8ebe | |||
| a4cc9211d6 | |||
| 836d9f3b8b | |||
| 61650e5b3b | |||
| 4e457aa570 | |||
| fe70da9c08 | |||
| 4b57e65658 | |||
| bcd81eeae4 | |||
| af483d1165 | |||
| 6b8651fda2 | |||
| de699aebe5 | |||
| 0021cf876b | |||
| 32b601bde6 | |||
| b464e7f80b | |||
| e0f282aca9 | |||
| ba65daf6cb | |||
| b8455f6de9 | |||
| 132c923f25 | |||
| b6d83a4f61 | |||
| 4526d6d831 | |||
| 02d2ba255c | |||
| d94792dd65 | |||
| 66eb75f492 | |||
| 33a2f70d07 | |||
| 10f942c8e0 | |||
| 568a9ce4d5 | |||
| ddf415a359 | |||
| 29ac9c1919 | |||
| d3bed7830c | |||
| c2d5f47643 | |||
| 731e92c8e4 | |||
| 94a600c0c1 | |||
| e1d8ae8a35 | |||
| d3b7186dea | |||
| 3608c137a0 | |||
| 15a3efe758 | |||
| 4b73ae462f | |||
| b49a631e0b | |||
| 12b6b04055 | |||
| a1f659bea0 | |||
| 8ca4cb8dcb | |||
| 8ce8f250d5 | |||
| 3672f6f92f | |||
| 888a6b2caa | |||
| 231039e86c | |||
| dc884e6052 | |||
| 4675589cf6 | |||
| 32f03bfd62 | |||
| d1bfd29dfd | |||
| 27ec594606 | |||
| f652a522fd | |||
| a794e87490 | |||
| 5b04d4fbe1 | |||
| 1566e193f6 | |||
| 601c8e9d52 | |||
| 8e5707dbcc | |||
| ad4d0c7b96 | |||
| a11e004943 | |||
| f605d86a4e | |||
| f8bde63081 | |||
| 887ce71005 | |||
| 87233ceafe | |||
| 27c15680e8 | |||
| cb72e2d70f | |||
| 2cafbc8484 | |||
| 6dbb28b2b8 | |||
| d8a4a28259 | |||
| 9a217171c8 | |||
| c67cafcf94 | |||
| f7057a9517 | |||
| ae5379d84a | |||
| c05068c2e8 | |||
| 22bc2f9d0f | |||
| c07bf2b108 | |||
| e447fc0f1e | |||
| e062df68c5 | |||
| 664a5cd675 | |||
| 9ef217b351 | |||
| 67bd15487d | |||
| ab18da351d | |||
| 1abb735bfa | |||
| 9397d8ab36 | |||
| 82300d6944 | |||
| cf3841e35c | |||
| 1a0bffdc51 | |||
| 666b5c9700 | |||
| 2eabc6045f | |||
| cc32e79f2a | |||
| 7074fc8856 | |||
| a7d85e5905 | |||
| cad4233d0d | |||
| 9b9894c07d | |||
| 8546cf31ce | |||
| aa0830ff0c | |||
| 4076941bd7 | |||
| 4170c9e995 | |||
| a862192bc4 | |||
| 5a18428666 | |||
| 0b5a627c84 | |||
| 65820cdf7a | |||
| 2c3a107e00 | |||
| e028db585f | |||
| d523ca8186 | |||
| f008f245dc | |||
| f2f3196368 | |||
| 29eccc9226 | |||
| 9abbd74a9f | |||
| 846fe5e08a | |||
| da29961052 | |||
| e5b8315859 | |||
| c6bb5f76f7 | |||
| fea7acb56e | |||
| 50d3676847 | |||
| 8830354027 | |||
| 2467674fca | |||
| 9691b0f518 | |||
| 005114839a | |||
| a5d322252a | |||
| b49630d6fc | |||
| 93650efddb | |||
| d87327835e | |||
| 0d90ff96b9 | |||
| b14595cb2d | |||
| e50a5e175f | |||
| a7d6782176 | |||
| eb6dd4d69e | |||
| a15d8a3f7b | |||
| 22cfa4c8c7 | |||
| 4e566b4692 | |||
| 9d0c7b5ade | |||
| 87a36633e9 | |||
| 0e0189f0eb | |||
| a880a88fc0 | |||
| 465312b8a0 | |||
| e319f3cd14 | |||
| e92afd8f78 | |||
| 280563caa8 | |||
| 3d8968eff3 | |||
| 1301847b7e | |||
| 09c56342d2 | |||
| ea80f6d49c | |||
| 622072bd50 | |||
| 1f469e23b5 | |||
| d8b9777b40 | |||
| e5e3c49ace | |||
| c6e0a06b14 | |||
| 75e5f95277 | |||
| a59fc6b8e8 | |||
| 4ff3c894fa | |||
| abfad37640 | |||
| 63b8cc9ad9 | |||
| a69c00a5bc | |||
| 4f229a5bcf | |||
| 83f7c869f0 | |||
| 397362bb1a | |||
| d5571c41c7 | |||
| 39921809ee | |||
| 68fe2bc852 | |||
| 398fec13b0 | |||
| 9fc7ebdac2 | |||
| 74bc3f5db3 | |||
| a5f8ce2503 | |||
| 2171c05ef8 | |||
| 8f8de4bd29 | |||
| c055611914 | |||
| 9b8f6b1d56 | |||
| 096fa47c6d | |||
| e677a2e84a | |||
| 69a1e2561d | |||
| 2df82e87b7 | |||
| cc1fc7ccfe | |||
| ba86851d07 | |||
| f9ae9b323a | |||
| 5e91b10c8c | |||
| 568345c396 | |||
| 8c3fe8dabb | |||
| ef6815222c | |||
| 0c76677b16 | |||
| 3b785e4165 | |||
| 320307f504 | |||
| 88ef8efba5 | |||
| 3fad19650d | |||
| 7d9f020ae8 | |||
| 2f540a8026 | |||
| b2d05828a5 | |||
| b7e91f0994 | |||
| 247f5e7c60 | |||
| 36121e5643 | |||
| 763b810ca8 | |||
| d80b905945 | |||
| e2cbd637b8 | |||
| d96ef0f178 | |||
| de181bef0f | |||
| 14e4f3dad8 | |||
| d2b9adf6f2 | |||
| 036422399a | |||
| 53c2f8b778 | |||
| 6050051e04 | |||
| 8c2ea639fd | |||
| 5ddb718eab | |||
| 9ca9569714 | |||
| 1357d246d8 | |||
| 03f189b62c | |||
| 8d8f4714c3 | |||
| c75c2c7594 | |||
| 17186b0c73 | |||
| 2aac7baf52 | |||
| bc090e1dce | |||
| 31ccce1343 | |||
| 147955c682 | |||
| 840b76228a | |||
| 8436001d5b | |||
| c86b888b0a | |||
| 0936922172 | |||
| 62a0a1a560 | |||
| 596f59d3c4 | |||
| e642847a82 | |||
| 26c14e218a | |||
| 26c775ccfd | |||
| d6284148f9 | |||
| 4c58266a87 | |||
| 2f06b0690c | |||
| 19d213d748 | |||
| c8fca7953c | |||
| 55d4d98353 | |||
| dd4f90e4c2 | |||
| 6647f6d0e0 | |||
| 9d05949d19 | |||
| 9da58b1ea2 | |||
| 38485a5b1e | |||
| bf4b660568 | |||
| c028d5e8cb | |||
| eafec74c29 | |||
| 74364544c2 | |||
| a4c53689ca | |||
| 0ceff44421 | |||
| f79423a4ee | |||
| c9309c926c | |||
| ee3124cfc6 | |||
| eb94211053 | |||
| bd01cd4033 | |||
| 21013a8317 | |||
| 451de79666 | |||
| 755fdfaca2 | |||
| f4057850b9 | |||
| a56f52ff18 | |||
| 4d6688d68e | |||
| 7a7ff1142c | |||
| 8cceed12f7 | |||
| f8f28e9554 | |||
| ac5ecf946d | |||
| d87d8bac92 | |||
| 9dffe791ad | |||
| 3057aed571 | |||
| 14c29ff912 | |||
| 73cdf5417b | |||
| fa0d2860f0 | |||
| dcbb2a06a5 | |||
| bd64232eb6 | |||
| 767bc6bd0b | |||
| 8ddb81eb5c | |||
| f16a68f59c | |||
| 0e6f9c38fb | |||
| 037da55de1 | |||
| 0666f8784f | |||
| 01f16d35c2 | |||
| f8e0a8d880 | |||
| ae0fa20aa6 | |||
| cefc09a41b | |||
| 7742980426 | |||
| 57639c4adf | |||
| 75ca51b23b | |||
| ce2966e547 | |||
| 861260198a | |||
| 3e7bec5604 | |||
| 14a343a0d7 | |||
| 704af8f2f0 | |||
| ac2dd1dfd3 | |||
| df18133179 | |||
| be6d06a653 | |||
| a998755245 | |||
| 98c82a3684 | |||
| 4658bec08b | |||
| 33047c2c55 | |||
| b2b7f11d17 | |||
| f0ad0f9496 | |||
| d6a156b181 | |||
| 880f487093 | |||
| bce54d5878 | |||
| dbc336f921 | |||
| 71085969f5 | |||
| c35cd783cf | |||
| 38ea720fc5 | |||
| 06b2b920da | |||
| a3078fa93e | |||
| 0bc45c822d | |||
| af5b04b24f | |||
| 678d0c61f1 | |||
| 37ee41af67 | |||
| 8b8eb57555 | |||
| d6efe8636e | |||
| caf55bb8e0 | |||
| cc4798472f | |||
| e3a95d5c4e | |||
| 997ffe9c31 | |||
| c6c607a406 | |||
| c25693b009 | |||
| fca153cc8b | |||
| 06cbd29d0d | |||
| aff7b0db50 | |||
| f7ff964085 | |||
| 94dc4f2365 | |||
| 09954b2c73 | |||
| 87fc8c16af | |||
| 75530810b5 | |||
| bdabfa1071 | |||
| 26a9219f7d | |||
| 5b92336668 | |||
| fb538333fa | |||
| 1d1bd41bb8 | |||
| 96e60ae540 | |||
| 4195ad90bc | |||
| eaa363eb82 | |||
| 795870bca5 | |||
| 7e73165409 | |||
| 2d6d371292 | |||
| d9168ed723 | |||
| 57fc7f7098 | |||
| 20c81d7f9a | |||
| 67d04ca878 | |||
| f5dda668e3 | |||
| 769150dec6 | |||
| 02d080955b | |||
| 04739a41fa | |||
| 9771747916 | |||
| 497196886a | |||
| 056df39a3c | |||
| 5e86a0b9e6 | |||
| ba2c86361d | |||
| b92ca5d6e5 | |||
| fe2c623638 | |||
| 0e9f047c37 | |||
| 582201ab7c | |||
| 88f4101866 | |||
| 373a157163 | |||
| 0178093f6c | |||
| 8e8cf90a71 | |||
| a22dc9be1b | |||
| 2306993b7b | |||
| c248dc1803 | |||
| 53f96c98ad | |||
| 98b1ed07d1 | |||
| b342af0d96 | |||
| 52faf78324 | |||
| 8fd48e9fa9 | |||
| 32a3943821 | |||
| f7f220aa18 | |||
| 372ce3c4bc | |||
| 95915480a0 | |||
| d2332e6452 | |||
| 1428f54a02 | |||
| feb87c0f19 | |||
| 934fa0598b | |||
| 6d6761e234 | |||
| ab18d4835b | |||
| ff8a02959c | |||
| 37a9fbe530 | |||
| 735d7a4d61 | |||
| 40eb4b4751 | |||
| 0a6e8e3c9e | |||
| 556d7fa4b8 | |||
| 5718f3f2f5 | |||
| 0251fddae4 | |||
| 0215925608 | |||
| 696b78f207 | |||
| 674f4f784b | |||
| 83a05a1900 | |||
| 48a0bca35e | |||
| a7690c375e | |||
| a0bd32a39c | |||
| 0b3dd2709b | |||
| ff1557afee | |||
| 67771795ca | |||
| 7a01c9c62d | |||
| 9768ecef22 | |||
| 640c202ee5 | |||
| f10acb3df8 | |||
| 5b74f82edb | |||
| 59bf4501e8 | |||
| f29449db14 | |||
| 78172d11d7 | |||
| f42ff697d4 | |||
| 57e2c5c670 | |||
| 8b41ec48c8 | |||
| 49ff832f94 | |||
| 09a44ba409 | |||
| deae90485a | |||
| 4a0bdcbb64 | |||
| 9f1860dff7 | |||
| fe25414b45 | |||
| 49184c9163 | |||
| 440c3debdc | |||
| 8029305d32 | |||
| 02bdb7c76a | |||
| 8d537b4e3e | |||
| 11401ca472 | |||
| 66429bff45 | |||
| 8114786179 | |||
| d8c1e432c6 | |||
| 3a286ae07f | |||
| a93ccd4100 | |||
| c1f7e5fe96 | |||
| d16110068c | |||
| 4661913265 | |||
| ecc88e7a40 | |||
| 5cf8c54cb2 | |||
| b758033ccb | |||
| 13f4bb441d | |||
| 1af09069f7 | |||
| b5bf2cbcda | |||
| fb3436b23d | |||
| 1a3cfea348 | |||
| c33141d49b | |||
| f9325fbc91 | |||
| 810df17009 | |||
| 19255debb9 | |||
| 1ebd37f9ad | |||
| 8e37813129 | |||
| 91574b53d9 | |||
| b8e0aba3cf | |||
| 0eccaf2b60 | |||
| 859edb10cd | |||
| f0bf519d36 | |||
| d8cfafccc9 | |||
| 34c1c1f83a | |||
| 3e1485faf5 | |||
| efc5573b8e | |||
| 45035802be | |||
| 2d97ba2bbd | |||
| 05fed16f6d | |||
| 9b1f4272c0 | |||
| 1987220f1e | |||
| 47696a3303 | |||
| 787f954833 | |||
| e9bdb983c8 | |||
| b4cd86aa14 | |||
| 4bd26f5977 | |||
| 57ff7c69cd | |||
| c843709eab | |||
| 4bc2180eed | |||
| 5353890b24 | |||
| 79975bf2b6 | |||
| 8d09c202db | |||
| 27013c0b2b | |||
| 756c499314 | |||
| 6c6d1b18ba | |||
| 8cb1236a8c | |||
| 5636a9990b | |||
| 8ab8398c50 | |||
| 53c4a567c0 | |||
| ff8f79f883 | |||
| 06be6f23a6 | |||
| 24e62b58bc | |||
| b37a3341e8 | |||
| eee6ccc311 | |||
| f233a21787 | |||
| 940538e39b | |||
| 3a29e08ff2 | |||
| 2a2e35c096 | |||
| 287a947c62 | |||
| 32595e7cb7 | |||
| 9deb031c44 | |||
| 6be4f5e8d0 | |||
| b5b0a52cbe | |||
| a0af6a25aa | |||
| c2ee086487 | |||
| 43a0ceb350 | |||
| 50eaf6c0bb | |||
| e564b7c150 | |||
| 943e76ae8b | |||
| 72a7780054 | |||
| 1001ca92c8 | |||
| d060ad7200 | |||
| eeb5bd79d0 | |||
| fad660450c | |||
| 01893a492f | |||
| e61a4dd53c | |||
| 183e6ec436 | |||
| ebd5aa5fe9 | |||
| 1b6e5e5b3b | |||
| 7b7ebc5711 | |||
| 92e16c81df | |||
| 5c1fd4dd2c | |||
| d7cc9b89d1 | |||
| a69aa7c533 | |||
| a71186bcc3 | |||
| aeef41bf70 | |||
| 99dbe31d20 | |||
| 5ca3a97bd3 | |||
| cfcab13c2a | |||
| f1649a647b | |||
| 11deb9425b | |||
| 3aaf0a8c44 | |||
| 322e5c1587 | |||
| ed45c44931 | |||
| 889fcc2158 | |||
| f155ed3ba9 | |||
| 0d20c59a98 | |||
| ae61706a34 | |||
| f4a152ab22 | |||
| 972bf6b3cf | |||
| 18203c614d | |||
| 40c242f9d1 | |||
| ee4c83e323 | |||
| 214e9075ad | |||
| 847fd90e36 | |||
| ecc055180c | |||
| 1501a4e531 | |||
| 8f53e01749 | |||
| 1df8cff842 | |||
| 76744ead86 | |||
| f6a7030654 | |||
| 7c8dd7362d | |||
| 0ae9884836 | |||
| 95ca6fb376 | |||
| c9b1782d62 | |||
| d1102e2e9c | |||
| 9c6f896666 | |||
| 0fdba29d51 | |||
| f74217ed9c | |||
| e25ade8af3 | |||
| bece12ad4d | |||
| 77e02bf7a3 | |||
| 90932cdac5 | |||
| 5b5307cc15 | |||
| aaa622288c | |||
| dbf1ac7867 | |||
| c187b0d52c | |||
| 8e62195eb5 | |||
| 0c749922c9 | |||
| ecc41f0c0f | |||
| 81582bffd2 | |||
| ced5ac7876 | |||
| 2020ca9971 | |||
| 352f4e8772 | |||
| 71fd730598 | |||
| e90ac6767f | |||
| 268c39ea8c | |||
| b3d9055059 | |||
| 904265120b | |||
| 47930a4214 | |||
| a59cad6f20 | |||
| 393d1a6888 | |||
| 42cfee2c09 | |||
| afdc10b919 | |||
| af899149c7 | |||
| abf8abb59b | |||
| f0a85b2b6e | |||
| 515e5532c8 | |||
| ff3bf4b180 | |||
| 1d4e6183be | |||
| 69d473ea6f | |||
| bfc86d3b30 | |||
| 3ce27743ae | |||
| c9f5763a70 | |||
| f61514edaf | |||
| 2aca913e92 | |||
| 26254a0ad8 | |||
| 6f130e1583 | |||
| 35ad6cacc8 | |||
| f519ecded7 | |||
| 4b2efeec7a | |||
| a687df2f4f | |||
| 29deae6fe0 | |||
| f9f1c8ee75 | |||
| a0544dc2b5 | |||
| 0a1798869a | |||
| 751fd2f9ba | |||
| 954b222fb6 | |||
| 4c04e77489 | |||
| 152538ccc1 | |||
| d955b06f03 | |||
| db19a56458 | |||
| 6539e1b856 | |||
| 8046fb1eb9 | |||
| 0302e39d57 | |||
| c29ccaadbc | |||
| d2d168f363 | |||
| cf98718a79 | |||
| 47bda055e0 | |||
| c1e577c1ac | |||
| 5b98039291 | |||
| 5a23f71a7f | |||
| c46f12f0d6 | |||
| f89350e6cd | |||
| fd404064c9 | |||
| 21029c326b | |||
| e8ec6385f3 | |||
| 1361bf20dd | |||
| 5d98546e1d | |||
| ff5eb53f48 | |||
| 814d4dbb51 | |||
| 2c68175b5c | |||
| 551a370c13 | |||
| 1afcd68e01 | |||
| cc30ef3ee2 | |||
| 235e35a2f3 | |||
| 4c54f967b7 | |||
| 8925949be3 | |||
| e96372c999 | |||
| e5d9d27069 | |||
| 853c990b6e | |||
| 86c1ffab2a | |||
| 952287a787 | |||
| b5d90b7b13 | |||
| 3e06af8515 | |||
| eff2e56d0d | |||
| eaac04ccf6 | |||
| 894192851e | |||
| f93bc6ef0f | |||
| 035d5314b0 | |||
| a40cfc679c | |||
| bda6eb4200 | |||
| 70f7d7409a | |||
| 48635c8057 | |||
| 122ff609e8 | |||
| 30e94064e5 | |||
| d60e575af5 | |||
| 91c3ed8ec4 | |||
| 0dc49d2a70 | |||
| a83dea705f | |||
| ed7a960e31 | |||
| 9bf1a44f75 | |||
| 8ecd01be1f | |||
| 4933e3caf4 | |||
| 4a9a5bc713 | |||
| 7412b84e04 | |||
| 3775d69a60 | |||
| 4cf57ae84d | |||
| ef2541776b | |||
| 78082bc10f | |||
| 5342e549cd | |||
| e2d1e9a7df | |||
| 9aa9917952 | |||
| c1cdb69f56 | |||
| 5b8c909ac3 | |||
| 20b038bfb7 | |||
| 9f9a1a138b | |||
| 4cb9ac5357 | |||
| cd76e481fd | |||
| 8e309b57b3 | |||
| beb9d43f4d | |||
| b281819cba | |||
| 6826efc723 | |||
| 370e3cdfea | |||
| fb8b05e7fd | |||
| 8c94973cf9 | |||
| 787cc8e3ee | |||
| ff51c46915 | |||
| 0914cd16af | |||
| 9b28d8984f | |||
| bd4c3a246d | |||
| 914ddf4859 | |||
| 2fa3adee1d | |||
| fd83c1a5bf | |||
| de2be9b8ae | |||
| 209b030502 | |||
| 5a65ce6adb | |||
| 79a7cf864e | |||
| 19f7b0fe8d | |||
| 2b7bb24c92 | |||
| faf2d588b1 | |||
| 8e02a03170 | |||
| d13de0464e | |||
| c0528888f4 | |||
| b4efe6a810 | |||
| f09a273ad7 | |||
| 3cd168c44d | |||
| b0ab8f417d | |||
| d7ccf95499 | |||
| 6ab5e60833 | |||
| e63d74dff2 | |||
| 25d7d7c037 | |||
| 1c7c07ace7 | |||
| f7f42a9cd8 | |||
| 1e7731c317 | |||
| 208a2151c1 | |||
| 4a6d53e16d | |||
| a778ff8903 | |||
| bb42a2158d | |||
| ab2f5420c6 | |||
| 174943cd0f | |||
| 0bc66032d2 | |||
| 899ed92ebc | |||
| 129eb8644d | |||
| d582f9bab2 | |||
| 0f1575d5af | |||
| f9c0d99790 | |||
| 1f56fb86e3 | |||
| f9d08e1377 | |||
| cbf0416d7e | |||
| 2422d3ab40 | |||
| 960cf76c42 | |||
| af2564c580 | |||
| b385b1e975 | |||
| ce1d4b852a | |||
| c43d31f693 | |||
| e4259c5045 | |||
| 993dd2ad1c | |||
| 3201244d9b | |||
| f4dc87fa44 | |||
| 24a027194e | |||
| 62c4dc1472 | |||
| b7f74b4e55 | |||
| 2da853dcbe | |||
| 0438a7c76b | |||
| c86c3a803f | |||
| 19db62d74f | |||
| 0146411483 | |||
| b731e17850 | |||
| bb66b2296d | |||
| c75ef8546e | |||
| d80c2b6104 | |||
| ec4bf35647 | |||
| 669d99bebc | |||
| 715af5950f | |||
| 7a5ac3dbc2 | |||
| ae61cbb4c0 | |||
| bde6e6bda4 | |||
| a656241d5e | |||
| 7ae5a12f4a | |||
| 769e984ef4 | |||
| 98e75f6d97 | |||
| 586f879e80 | |||
| e2112670bf | |||
| 060c6669c1 | |||
| aa02c5cbe9 | |||
| 77d7c3871a | |||
| 67e38955ae | |||
| 26098f6c8d | |||
| 021b6f694b | |||
| f0a370db24 | |||
| 2e7f624b34 | |||
| b63c0c7dfc | |||
| 906cbf1e6f | |||
| 5aefc741f2 | |||
| 5c28ea3064 | |||
| 70cd11e30f | |||
| 94a72b23ff | |||
| d08be990ef | |||
| ca5b7758ce | |||
| bb1def2910 | |||
| 673abae1be | |||
| 3b8c067e70 | |||
| 528459eeb2 | |||
| bc1ab4e55c | |||
| 5aefd0b393 | |||
| 7bc53a4253 | |||
| 8b12dccd76 | |||
| 952afb6040 | |||
| 56ca5e9e62 | |||
| a834461752 | |||
| 37166af321 | |||
| 2c91bcdc30 | |||
| e2d6a53de5 | |||
| 89537b1521 | |||
| 152b3cae90 | |||
| f104e53fd4 | |||
| fd99ae0e59 | |||
| 4b6c159e3a | |||
| 50f8cfbc15 | |||
| 22848b010d | |||
| 3b5840e248 | |||
| f955707201 | |||
| 86dc10028f | |||
| a64807efda | |||
| 61e00b5fbd | |||
| 1b2fccb615 | |||
| 702c1ff217 | |||
| d1b2814ec6 | |||
| ec57d1786c | |||
| 9f7c1f90ec | |||
| e23e30eb44 | |||
| 2b4a61b84c | |||
| fbafde6e27 | |||
| 14e5a75d35 | |||
| ea32d8627e | |||
| 814a0498df | |||
| e45f85cc1e | |||
| 1e9c032072 | |||
| c741cca5f2 | |||
| 72ff71f59c | |||
| 699323e2e0 | |||
| fb5985bbbe | |||
| 5a9f4bee12 | |||
| 4717793d8e | |||
| de38011dd2 | |||
| 0f4bbc4752 | |||
| 86a1c41e5d | |||
| 021b8c4cff | |||
| bcacb7b7f0 | |||
| 747b9752ce | |||
| b0be6c8a7a | |||
| 58c7358113 | |||
| b1a2523ef0 | |||
| b7323db31b | |||
| 2e52692ba6 | |||
| 0075ead526 | |||
| 6df4a746f1 | |||
| 074904ee92 | |||
| 108b0ea226 | |||
| 9a704de43b | |||
| 7dfc12d138 | |||
| 9000446663 | |||
| 814ac6c28c | |||
| d1a284298f | |||
| 9509629bcf | |||
| f1882cfe2c | |||
| 90e446ec16 | |||
| 464ed8269b | |||
| 57a51d94ed | |||
| 53c557271d | |||
| b6fe16095b | |||
| 6a1c439325 | |||
| 06b0be7bad | |||
| e5acf22285 | |||
| 5f904a164c | |||
| 9a30a11786 |
@@ -0,0 +1,10 @@
|
||||
.go/
|
||||
.git/
|
||||
obj-x86_64-linux-gnu/
|
||||
obj-aarch64-linux-gnu/
|
||||
obj-arm-linux-gnueabihf/
|
||||
obj-i686-linux-gnu/
|
||||
unit.out
|
||||
aptly.test
|
||||
build/
|
||||
dpkgs/
|
||||
@@ -0,0 +1,7 @@
|
||||
[flake8]
|
||||
max-line-length = 240
|
||||
ignore = E126,E241,E741,W504
|
||||
include =
|
||||
system
|
||||
exclude =
|
||||
system/env
|
||||
@@ -0,0 +1 @@
|
||||
github: aptly-dev
|
||||
@@ -4,6 +4,10 @@ Fixes #
|
||||
|
||||
All new code should be covered with tests, documentation should be updated. CI should pass.
|
||||
|
||||
Also, to speed up things, if you could kindly "Allow edits and access to secrets by maintainers" in the
|
||||
PR settings, as this allows us to rebase the PR on master, fix conflicts, run coverage and help with
|
||||
implementing code and tests.
|
||||
|
||||
## Description of the Change
|
||||
|
||||
<!--
|
||||
@@ -14,6 +18,7 @@ Why this change is important?
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] allow Maintainers to edit PR (rebase, run coverage, help with tests, ...)
|
||||
- [ ] unit-test added (if change is algorithm)
|
||||
- [ ] functional test added/updated (if change is functional)
|
||||
- [ ] man page updated (if applicable)
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# see: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell
|
||||
shell: bash --noprofile --norc -eo pipefail {0}
|
||||
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: "Unit Tests"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
- name: "Docker Image"
|
||||
run: |
|
||||
make docker-image
|
||||
- name: "Unit Tests"
|
||||
run: |
|
||||
make docker-unit-tests
|
||||
mkdir -p out/coverage
|
||||
mv unit.out out/coverage/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
path: out/
|
||||
|
||||
test:
|
||||
name: "System Test"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
NO_FTP_ACCESS: yes
|
||||
BOTO_CONFIG: /dev/null
|
||||
GO111MODULE: "on"
|
||||
GOPROXY: "https://proxy.golang.org"
|
||||
|
||||
steps:
|
||||
- name: "Install Test Packages"
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends graphviz gnupg2 gpgv2 git gcc make devscripts python3 python3-requests-unixsocket python3-termcolor python3-swiftclient python3-boto python3-azure-storage python3-etcd3 python3-plyvel flake8 faketime
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Run flake8"
|
||||
run: |
|
||||
make flake8
|
||||
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
gover=$(sed -n 's/^go \(.*\)/\1/p' go.mod)
|
||||
echo "Go Version: $gover"
|
||||
echo "GOVER=$gover" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ steps.goversion.outputs.GOVER }}
|
||||
|
||||
- name: "Install Azurite"
|
||||
id: azuright
|
||||
uses: potatoqualitee/azuright@v1.1
|
||||
with:
|
||||
directory: ${{ runner.temp }}
|
||||
|
||||
- name: "Run Benchmark"
|
||||
run: |
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make bench
|
||||
|
||||
- name: "Run System Tests"
|
||||
env:
|
||||
RUN_LONG_TESTS: 'yes'
|
||||
AZURE_STORAGE_ENDPOINT: "http://127.0.0.1:10000/devstoreaccount1"
|
||||
AZURE_STORAGE_ACCOUNT: "devstoreaccount1"
|
||||
AZURE_STORAGE_ACCESS_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
sudo mkdir -p /srv ; sudo chown runner /srv
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make system-test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
path: out/
|
||||
|
||||
coverage:
|
||||
name: "Upload Coverage"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- unit-test
|
||||
- test
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Download Unit Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
|
||||
- name: "Download System Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
|
||||
- name: "Merge Code Coverage"
|
||||
run: |
|
||||
# go install github.com/wadey/gocovmerge@v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
# ~/go/bin/gocovmerge coverage/*.out > coverage.txt
|
||||
awk 'FNR==1 && NR!=1 {next} {print}' coverage/*.out > coverage.txt
|
||||
|
||||
- name: "Upload Code Coverage"
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.txt
|
||||
fail_ci_if_error: true
|
||||
|
||||
|
||||
ci-debian-build:
|
||||
name: "Build"
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: ["Debian 13/trixie", "Debian 12/bookworm", "Debian 11/bullseye", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
|
||||
arch: ["amd64", "i386" , "arm64" , "armhf"]
|
||||
include:
|
||||
- name: "Debian 13/trixie"
|
||||
suite: trixie
|
||||
image: debian:trixie-slim
|
||||
- name: "Debian 12/bookworm"
|
||||
suite: bookworm
|
||||
image: debian:bookworm-slim
|
||||
- name: "Debian 11/bullseye"
|
||||
suite: bullseye
|
||||
image: debian:bullseye-slim
|
||||
- name: "Ubuntu 24.04"
|
||||
suite: noble
|
||||
image: ubuntu:24.04
|
||||
- name: "Ubuntu 22.04"
|
||||
suite: jammy
|
||||
image: ubuntu:22.04
|
||||
- name: "Ubuntu 20.04"
|
||||
suite: focal
|
||||
image: ubuntu:20.04
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
env:
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
steps:
|
||||
- name: "Install Build Packages"
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends make ca-certificates git curl build-essential devscripts dh-golang jq bash-completion lintian \
|
||||
binutils-i686-linux-gnu binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf \
|
||||
libc6-dev-i386-cross libc6-dev-armhf-cross libc6-dev-arm64-cross \
|
||||
gcc-i686-linux-gnu gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
gover=$(sed -n 's/^go \(.*\)/\1/p' go.mod)
|
||||
echo "Go Version: $gover"
|
||||
echo "GOVER=$gover" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ steps.goversion.outputs.GOVER }}
|
||||
|
||||
- name: "Ensure CI build"
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo "FORCE_CI=true" >> $GITHUB_OUTPUT
|
||||
id: force_ci
|
||||
|
||||
- name: "Build Debian packages"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
make dpkg DEBARCH=${{ matrix.arch }}
|
||||
|
||||
- name: "Check aptly credentials"
|
||||
env:
|
||||
APTLY_USER: ${{ secrets.APTLY_USER }}
|
||||
APTLY_PASSWORD: ${{ secrets.APTLY_PASSWORD }}
|
||||
run: |
|
||||
found=no
|
||||
if [ -n "$APTLY_USER" ] && [ -n "$APTLY_PASSWORD" ]; then
|
||||
found=yes
|
||||
fi
|
||||
echo "Aptly credentials available: $found"
|
||||
echo "FOUND=$found" >> $GITHUB_OUTPUT
|
||||
id: aptlycreds
|
||||
|
||||
- name: "Publish CI release to aptly"
|
||||
if: github.ref == 'refs/heads/master' && steps.aptlycreds.outputs.FOUND == 'yes'
|
||||
env:
|
||||
APTLY_USER: ${{ secrets.APTLY_USER }}
|
||||
APTLY_PASSWORD: ${{ secrets.APTLY_PASSWORD }}
|
||||
run: |
|
||||
.github/workflows/scripts/upload-artifacts.sh ci ${{ matrix.suite }}
|
||||
|
||||
- name: "Publish release to aptly"
|
||||
if: startsWith(github.event.ref, 'refs/tags') && steps.aptlycreds.outputs.FOUND == 'yes'
|
||||
env:
|
||||
APTLY_USER: ${{ secrets.APTLY_USER }}
|
||||
APTLY_PASSWORD: ${{ secrets.APTLY_PASSWORD }}
|
||||
run: |
|
||||
.github/workflows/scripts/upload-artifacts.sh release ${{ matrix.suite }}
|
||||
|
||||
- name: "Get aptly version"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
aptlyver=$(make -s version)
|
||||
echo "Aptly Version: $aptlyver"
|
||||
echo "VERSION=$aptlyver" >> $GITHUB_OUTPUT
|
||||
id: releaseversion
|
||||
|
||||
- name: "Upload CI Artifacts"
|
||||
if: github.ref != 'refs/heads/master' && !startsWith(github.event.ref, 'refs/tags')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.suite }}_${{ matrix.arch }}
|
||||
path: build/
|
||||
retention-days: 7
|
||||
|
||||
ci-binary-build:
|
||||
name: "Build"
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, freebsd, darwin]
|
||||
goarch: ["386", "amd64", "arm", "arm64"]
|
||||
exclude:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
echo "GOVER=$(sed -n 's/^go \(.*\)/\1/p' go.mod)" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ steps.goversion.outputs.GOVER }}
|
||||
|
||||
- name: "Ensure CI build"
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo "FORCE_CI=true" >> $GITHUB_OUTPUT
|
||||
id: force_ci
|
||||
|
||||
- name: "Get aptly version"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
aptlyver=$(make -s version)
|
||||
echo "Aptly Version: $aptlyver"
|
||||
echo "VERSION=$aptlyver" >> $GITHUB_OUTPUT
|
||||
id: releaseversion
|
||||
|
||||
- name: "Build aptly ${{ matrix.goos }}/${{ matrix.goarch }}"
|
||||
env:
|
||||
GOBIN: /usr/local/bin
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
make binaries GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}
|
||||
|
||||
- name: "Upload Artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(github.event.ref, 'refs/tags')
|
||||
with:
|
||||
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
path: build/aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip
|
||||
compression-level: 0 # no compression
|
||||
|
||||
- name: "Upload CI Artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: "!startsWith(github.event.ref, 'refs/tags')"
|
||||
with:
|
||||
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
path: build/aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip
|
||||
compression-level: 0 # no compression
|
||||
retention-days: 7
|
||||
|
||||
gh-release:
|
||||
name: "Github Release"
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: false
|
||||
needs: ci-binary-build
|
||||
if: startsWith(github.event.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Get aptly version"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
aptlyver=$(make -s version)
|
||||
echo "Aptly Version: $aptlyver"
|
||||
echo "VERSION=$aptlyver" >> $GITHUB_OUTPUT
|
||||
id: releaseversion
|
||||
|
||||
- name: "Download Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: out/
|
||||
|
||||
- name: "Create Release Notes"
|
||||
run: |
|
||||
echo -e "## Changes\n\n" > out/release-notes.md
|
||||
dpkg-parsechangelog -S Changes | tail -n +4 >> out/release-notes.md
|
||||
|
||||
- name: "Release"
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "Aptly Release ${{ steps.releaseversion.outputs.VERSION }}"
|
||||
files: "out/**/aptly_*.zip"
|
||||
body_path: "out/release-notes.md"
|
||||
@@ -0,0 +1,72 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Read go version from go.mod"
|
||||
run: |
|
||||
echo "GOVER=$(sed -n 's/^go \(.*\)/\1/p' go.mod)" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ steps.goversion.outputs.GOVER }}
|
||||
|
||||
- name: Create VERSION file
|
||||
run: |
|
||||
make -s version | tr -d '\n' > VERSION
|
||||
shell: sh
|
||||
|
||||
- name: Install and initialize swagger
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init -q --propertyStrategy pascalcase --markdownFiles docs
|
||||
shell: sh
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: v1.64.5
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#
|
||||
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
|
||||
# The location of the configuration file can be changed by using `--config=`
|
||||
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true, then all caching functionality will be completely disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
|
||||
# install-mode: "goinstall"
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
builds=build/
|
||||
packages=${builds}*.deb
|
||||
folder=`mktemp -u tmp.XXXXXXXXXXXXXXX`
|
||||
aptly_user="$APTLY_USER"
|
||||
aptly_password="$APTLY_PASSWORD"
|
||||
aptly_api="https://aptly-ops.aptly.info"
|
||||
version=`make version`
|
||||
|
||||
action=$1
|
||||
dist=$2
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 ci buster|bullseye|bookworm|focal|jammy|noble" >&2
|
||||
echo " $0 release" >&2
|
||||
}
|
||||
|
||||
# repos and publish must be created beforehand:
|
||||
#!/bin/sh
|
||||
#for dist in buster bullseye bookworm focal jammy noble
|
||||
#do
|
||||
# for build in ci release
|
||||
# do
|
||||
# echo
|
||||
# echo "# Creating and publishing $build/$dist"
|
||||
# aptly repo create -distribution=$dist -component=main aptly-$build-$dist
|
||||
# aptly publish repo -multi-dist -architectures="amd64,i386,arm64,armhf" -acquire-by-hash -component=main \
|
||||
# -distribution=$dist -batch -keyring=aptly.pub \
|
||||
# aptly-$build-$dist \
|
||||
# s3:repo.aptly.info:$build
|
||||
# done
|
||||
#done
|
||||
|
||||
if [ -z "$action" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "action" = "ci" ] && [ -z "$dist" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$aptly_user" ] || [ -z "$aptly_password" ]; then
|
||||
usage
|
||||
echo Error: please set APTLY_USER and APTLY_PASSWORD
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Publishing version '$version' to $action for $dist...\n"
|
||||
|
||||
upload()
|
||||
{
|
||||
echo "\nUploading files:"
|
||||
for file in $packages; do
|
||||
echo " - $file"
|
||||
jsonret=`curl -fsS -X POST -F "file=@$file" -u $aptly_user:$aptly_password ${aptly_api}/api/files/$folder`
|
||||
done
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo "\nCleanup..."
|
||||
jsonret=`curl -fsS -X DELETE -u $aptly_user:$aptly_password ${aptly_api}/api/files/$folder`
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sleeptime=5
|
||||
retries=60
|
||||
wait_task()
|
||||
{
|
||||
_id=$1
|
||||
_success=0
|
||||
sleep $sleeptime
|
||||
for t in `seq $retries`
|
||||
do
|
||||
jsonret=`curl -fsS -u $aptly_user:$aptly_password ${aptly_api}/api/tasks/$_id`
|
||||
_state=`echo $jsonret | jq .State`
|
||||
if [ "$_state" = "2" ]; then
|
||||
_success=1
|
||||
curl -fsS -X DELETE -u $aptly_user:$aptly_password ${aptly_api}/api/tasks/$_id
|
||||
break
|
||||
fi
|
||||
if [ "$_state" = "3" ]; then
|
||||
echo Error: task failed
|
||||
return 1
|
||||
fi
|
||||
sleep $sleeptime
|
||||
done
|
||||
if [ "$_success" -ne 1 ]; then
|
||||
echo Error: task timeout
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
add_packages() {
|
||||
_aptly_repository=$1
|
||||
_folder=$2
|
||||
jsonret=`curl -fsS -X POST -u $aptly_user:$aptly_password ${aptly_api}/api/repos/$_aptly_repository/file/$_folder?_async=true`
|
||||
_task_id=`echo $jsonret | jq .ID`
|
||||
wait_task $_task_id
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "Error: adding packages to $_aptly_repository failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_publish() {
|
||||
_publish=$1
|
||||
_dist=$2
|
||||
jsonret=`curl -fsS -X PUT -H 'Content-Type: application/json' --data \
|
||||
'{"AcquireByHash": true, "MultiDist": true,
|
||||
"Signing": {"Batch": true, "Keyring": "aptly.repo/aptly.pub", "secretKeyring": "aptly.repo/aptly.sec", "PassphraseFile": "aptly.repo/passphrase"}}' \
|
||||
-u $aptly_user:$aptly_password ${aptly_api}/api/publish/$_publish/$_dist?_async=true`
|
||||
_task_id=`echo $jsonret | jq .ID`
|
||||
wait_task $_task_id
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "Error: publish failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$action" = "ci" ]; then
|
||||
if echo "$version" | grep -vq "+"; then
|
||||
# skip ci when on release tag
|
||||
exit 0
|
||||
fi
|
||||
|
||||
aptly_repository=aptly-ci-$dist
|
||||
aptly_published=s3:repo.aptly.info:ci
|
||||
|
||||
elif [ "$action" = "release" ]; then
|
||||
aptly_repository=aptly-release-$dist
|
||||
aptly_published=s3:repo.aptly.info:release
|
||||
fi
|
||||
|
||||
upload
|
||||
|
||||
echo "\nAdding packages to $aptly_repository ..."
|
||||
add_packages $aptly_repository $folder
|
||||
|
||||
echo "\nUpdating published repo $aptly_published ..."
|
||||
update_publish $aptly_published $dist
|
||||
|
||||
# if [ "$action" = "OBSOLETErelease" ]; then
|
||||
# aptly_repository=aptly-release
|
||||
# aptly_snapshot=aptly-$version
|
||||
# aptly_published=s3:repo.aptly.info:./squeeze
|
||||
#
|
||||
# echo "\nAdding packages to $aptly_repository..."
|
||||
# curl -fsS -X POST -u $aptly_user:$aptly_password ${aptly_api}/api/repos/$aptly_repository/file/$folder
|
||||
# echo
|
||||
#
|
||||
# echo "\nCreating snapshot $aptly_snapshot from repo $aptly_repository..."
|
||||
# curl -fsS -X POST -u $aptly_user:$aptly_password -H 'Content-Type: application/json' --data \
|
||||
# "{\"Name\":\"$aptly_snapshot\"}" ${aptly_api}/api/repos/$aptly_repository/snapshots
|
||||
# echo
|
||||
#
|
||||
# echo "\nSwitching published repo $aptly_published to use snapshot $aptly_snapshot..."
|
||||
# curl -fsS -X PUT -H 'Content-Type: application/json' --data \
|
||||
# "{\"AcquireByHash\": true, \"Snapshots\": [{\"Component\": \"main\", \"Name\": \"$aptly_snapshot\"}],
|
||||
# \"Signing\": {\"Batch\": true, \"Keyring\": \"aptly.repo/aptly.pub\",
|
||||
# \"secretKeyring\": \"aptly.repo/aptly.sec\", \"PassphraseFile\": \"aptly.repo/passphrase\"}}" \
|
||||
# -u $aptly_user:$aptly_password ${aptly_api}/api/publish/$aptly_published
|
||||
# echo
|
||||
# fi
|
||||
|
||||
+43
-2
@@ -2,11 +2,14 @@
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
unit.out
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
tmp/
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
@@ -23,6 +26,8 @@ _testmain.go
|
||||
*.test
|
||||
|
||||
coverage.txt
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
*.pyc
|
||||
|
||||
@@ -32,6 +37,42 @@ root/
|
||||
man/aptly.1.html
|
||||
man/aptly.1.ronn
|
||||
|
||||
.goxc.local.json
|
||||
|
||||
system/env/
|
||||
|
||||
# created by make build for release artifacts
|
||||
VERSION
|
||||
aptly.test
|
||||
|
||||
build/
|
||||
|
||||
system/files/aptly2.gpg~
|
||||
system/files/aptly2_passphrase.gpg~
|
||||
|
||||
*.creds
|
||||
|
||||
.go/
|
||||
obj-x86_64-linux-gnu/
|
||||
obj-aarch64-linux-gnu/
|
||||
obj-arm-linux-gnueabihf/
|
||||
obj-i686-linux-gnu/
|
||||
|
||||
# debian
|
||||
debian/.debhelper/
|
||||
debian/aptly.substvars
|
||||
debian/aptly/
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/aptly-api/
|
||||
debian/*.debhelper
|
||||
debian/*.debhelper.log
|
||||
debian/aptly-api.substvars
|
||||
debian/aptly-dbg.substvars
|
||||
debian/aptly-dbg/
|
||||
usr/bin/aptly
|
||||
dpkgs/
|
||||
debian/changelog.dpkg-bak
|
||||
|
||||
docs/docs.go
|
||||
docs/swagger.json
|
||||
docs/swagger.yaml
|
||||
docs/swagger.conf
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "2"
|
||||
linters:
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
- "all"
|
||||
- "-QF1004" # could use strings.ReplaceAll instead
|
||||
- "-QF1012" # Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...))
|
||||
- "-QF1003" # could use tagged switch
|
||||
- "-ST1000" # at least one file in a package should have a package comment
|
||||
- "-QF1001" # could apply De Morgan's law
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"AppName": "aptly",
|
||||
"ArtifactsDest": "xc-out/",
|
||||
"TasksExclude": [
|
||||
"rmbin",
|
||||
"go-test",
|
||||
"go-vet"
|
||||
],
|
||||
"TasksAppend": [
|
||||
"bintray"
|
||||
],
|
||||
"TaskSettings": {
|
||||
"debs": {
|
||||
"metadata": {
|
||||
"maintainer": "Andrey Smirnov",
|
||||
"maintainer-email": "me@smira.ru",
|
||||
"description": "Debian repository management tool"
|
||||
},
|
||||
"metadata-deb": {
|
||||
"License": "MIT",
|
||||
"Homepage": "https://www.aptly.info/",
|
||||
"Depends": "bzip2, xz-utils, gnupg, gpgv",
|
||||
"Suggests": "graphviz"
|
||||
},
|
||||
"other-mapped-files": {
|
||||
"/": "root/"
|
||||
}
|
||||
},
|
||||
"bintray": {
|
||||
"repository": "aptly",
|
||||
"subject": "smira",
|
||||
"package": "aptly",
|
||||
"downloadspage": "bintray.md"
|
||||
}
|
||||
},
|
||||
"ResourcesInclude": "README.rst,LICENSE,AUTHORS,man/aptly.1",
|
||||
"Arch": "386 amd64",
|
||||
"Os": "linux darwin freebsd",
|
||||
"MainDirsExclude": "_man,vendor",
|
||||
"BuildSettings": {
|
||||
"LdFlagsXVars": {
|
||||
"Version": "main.Version"
|
||||
}
|
||||
},
|
||||
"ConfigVersion": "0.9"
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
dist: trusty
|
||||
sudo: required
|
||||
|
||||
language: go
|
||||
|
||||
go_import_path: github.com/aptly-dev/aptly
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python-virtualenv
|
||||
- graphviz
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: "EcCzJsqQ3HnIkprBPS1YHErsETcb7KQFBYEzVDE7RYDApWeapLq+r/twMtWMd/fkGeLzr3kWSg7nhSadeHMLYeMl9j+U7ncC5CWG5NMBOj/jowlb9cMCCDlmzMoZLAgR6jm1cJyrWCLsWVlv+D0ZiB0fx4xaBZP/gIr9g6nEwC8="
|
||||
- secure: "OxiVNmre2JzUszwPNNilKDgIqtfX2gnRSsVz6nuySB1uO2yQsOQmKWJ9cVYgH2IB5H8eWXKOhexcSE28kz6TPLRuEcU9fnqKY3uEkdwm7rJfz9lf+7C4bJEUdA1OIzJppjnWUiXxD7CEPL1DlnMZM24eDQYqa/4WKACAgkK53gE="
|
||||
- NO_FTP_ACCESS: "yes"
|
||||
- BOTO_CONFIG: /dev/null
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
env: RUN_LONG_TESTS=no
|
||||
fast_finish: true
|
||||
include:
|
||||
- go: 1.8.x
|
||||
env: RUN_LONG_TESTS=no
|
||||
- go: 1.9.x
|
||||
env: RUN_LONG_TESTS=yes
|
||||
- go: 1.10.x
|
||||
env: RUN_LONG_TESTS=yes
|
||||
- go: master
|
||||
env: RUN_LONG_TESTS=no
|
||||
|
||||
before_install:
|
||||
- virtualenv system/env
|
||||
- . system/env/bin/activate
|
||||
- pip install six packaging appdirs
|
||||
- pip install -U pip setuptools
|
||||
- pip install -r system/requirements.txt
|
||||
- make version
|
||||
|
||||
install:
|
||||
- make prepare
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- "https://webhooks.gitter.im/e/c691da114a41eed6ec45"
|
||||
on_success: change # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
@@ -33,3 +33,54 @@ List of contributors, in chronological order:
|
||||
* Petr Jediny (https://github.com/pjediny)
|
||||
* Maximilian Stein (https://github.com/steinymity)
|
||||
* Strajan Sebastian (https://github.com/strajansebastian)
|
||||
* Artem Smirnov (https://github.com/urpylka)
|
||||
* William Manley (https://github.com/wmanley)
|
||||
* Shengjing Zhu (https://github.com/zhsj)
|
||||
* Nabil Bendafi (https://github.com/nabilbendafi)
|
||||
* Raphael Medaer (https://github.com/rmedaer)
|
||||
* Raul Benencia (https://github.com/rul)
|
||||
* Don Kuntz (https://github.com/dkuntz2)
|
||||
* Joshua Colson (https://github.com/freakinhippie)
|
||||
* Andre Roth (https://github.com/neolynx)
|
||||
* Lorenzo Bolla (https://github.com/lbolla)
|
||||
* Benj Fassbind (https://github.com/randombenj)
|
||||
* Markus Muellner (https://github.com/mmianl)
|
||||
* Chuan Liu (https://github.com/chuan)
|
||||
* Samuel Mutel (https://github.com/smutel)
|
||||
* Russell Greene (https://github.com/russelltg)
|
||||
* Wade Simmons (https://github.com/wadey)
|
||||
* Steven Stone (https://github.com/smstone)
|
||||
* Josh Bayfield (https://github.com/jbayfield)
|
||||
* Boxjan (https://github.com/boxjan)
|
||||
* Mauro Regli (https://github.com/reglim)
|
||||
* Alexander Zubarev (https://github.com/strike)
|
||||
* Nicolas Dostert (https://github.com/acdn-ndostert)
|
||||
* Ryan Gonzalez (https://github.com/refi64)
|
||||
* Paul Cacheux (https://github.com/paulcacheux)
|
||||
* Nic Waller (https://github.com/sf-nwaller)
|
||||
* iofq (https://github.com/iofq)
|
||||
* Noa Resare (https://github.com/nresare)
|
||||
* Ramon N.Rodriguez (https://github.com/runitonmetal)
|
||||
* Golf Hu (https://github.com/hudeng-go)
|
||||
* Cookie Fei (https://github.com/wuhuang26)
|
||||
* Andrey Loukhnov (https://github.com/aol-nnov)
|
||||
* Christoph Fiehe (https://github.com/cfiehe)
|
||||
* Blake Kostner (https://github.com/btkostner)
|
||||
* Leigh London (https://github.com/leighlondon)
|
||||
* Gordian Schoenherr (https://github.com/schoenherrg)
|
||||
* Silke Hofstra (https://github.com/silkeh)
|
||||
* Itay Porezky (https://github.com/itayporezky)
|
||||
* Alejandro Guijarro Monerris (https://github.com/alguimodd)
|
||||
* JupiterRider (https://github.com/JupiterRider)
|
||||
* Agustin Henze (https://github.com/agustinhenze)
|
||||
* Tobias Assarsson (https://github.com/daedaluz)
|
||||
* Yaksh Bariya (https://github.com/thunder-coding)
|
||||
* Juan Calderon-Perez (https://github.com/gaby)
|
||||
* Ato Araki (https://github.com/atotto)
|
||||
* Roman Lebedev (https://github.com/LebedevRI)
|
||||
* Brian Witt (https://github.com/bwitt)
|
||||
* Ales Bregar (https://github.com/abregar)
|
||||
* Tim Foerster (https://github.com/tonobo)
|
||||
* Zhang Xiao (https://github.com/xzhang1)
|
||||
* Tom Nguyen (https://github.com/lecafard)
|
||||
* Philip Cramer (https://github.com/PhilipCramer)
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [team@aptly.info](mailto:team@aptly.info). All
|
||||
reported by contacting the project team on [Aptly Discussions](https://github.com/aptly-dev/aptly/discussions). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
+94
-81
@@ -2,7 +2,7 @@
|
||||
|
||||
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
The following is a set of guidelines for contributing to [aptly](https://github.com/smira/aplty) and related repositories, which are hosted in the [aptly-dev Organization](https://github.com/aptly-dev) on GitHub.
|
||||
The following is a set of guidelines for contributing to [aptly](https://github.com/aptly-dev/aplty) and related repositories, which are hosted in the [aptly-dev Organization](https://github.com/aptly-dev) on GitHub.
|
||||
These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## What should I know before I get started?
|
||||
@@ -11,7 +11,7 @@ These are just guidelines, not rules. Use your best judgment, and feel free to p
|
||||
|
||||
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
|
||||
By participating, you are expected to uphold this code.
|
||||
Please report unacceptable behavior to [team@aptly.info](mailto:team@aptly.info).
|
||||
Please report unacceptable behavior on [https://github.com/aptly-dev/aptly/discussions](https://github.com/aptly-dev/aptly/discussions)
|
||||
|
||||
### List of Repositories
|
||||
|
||||
@@ -40,7 +40,7 @@ Please report unacceptable behavior to [team@aptly.info](mailto:team@aptly.info)
|
||||
|
||||
There are two kinds of documentation:
|
||||
|
||||
* [aptly website](https://www.aptly/info)
|
||||
* [aptly website](https://www.aptly.info)
|
||||
* aptly `man` page
|
||||
|
||||
Core content is mostly the same, but website contains more information, tutorials, examples.
|
||||
@@ -60,7 +60,7 @@ If you want to update website, please follow steps below:
|
||||
We're always looking for new contributions to [FAQ](https://www.aptly.info/doc/faq/), [tutorials](https://www.aptly.info/tutorial/),
|
||||
general fixes, clarifications, misspellings, grammar mistakes!
|
||||
|
||||
### Your Fist Code Contribution
|
||||
### Code Contribution
|
||||
|
||||
Please follow [next section](#development-setup) on development process. When change is ready, please submit PR
|
||||
following [PR template](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
@@ -68,61 +68,117 @@ following [PR template](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
Make sure that purpose of your change is clear, all the tests and checks pass, and all new code is covered with tests
|
||||
if that is possible.
|
||||
|
||||
### Get the Source
|
||||
|
||||
To clone the git repo, run the following commands:
|
||||
```
|
||||
git clone git@github.com:aptly-dev/aptly.git
|
||||
cd aptly
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
This section describes local setup to start contributing to aptly source.
|
||||
Working on aptly code can be done locally on the development machine, or for convenience by using docker. The next sections describe the setup process.
|
||||
|
||||
### Go & Python
|
||||
### Docker Development Setup
|
||||
|
||||
You would need `Go` (latest version is recommended) and `Python` 2.7.x (3.x is not supported yet).
|
||||
This section describes the docker setup to start contributing to aptly.
|
||||
|
||||
If you're new to Go, follow [getting started guide](https://golang.org/doc/install) to install it and perform
|
||||
initial setup. With Go 1.8+, default `$GOPATH` is `$HOME/go`, so rest of this document assumes that.
|
||||
#### Dependencies
|
||||
|
||||
Usually `$GOPATH/bin` is appended to your `$PATH` to make it easier to run built binaries, but you might choose
|
||||
to prepend it or to skip this test if you're security conscious.
|
||||
Install the following on your development machine:
|
||||
- docker
|
||||
- make
|
||||
- git
|
||||
|
||||
### Forking and Cloning
|
||||
##### Docker installation on macOS
|
||||
1. Install [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) (or via [Homebrew](https://brew.sh/))
|
||||
2. Allow directory sharing
|
||||
- Open Docker Desktop
|
||||
- Go to `Settings → Resources → File Sharing → Virtual File Shares`
|
||||
- Add the aptly git repository path to the shared list (eg. /home/Users/john/aptly)
|
||||
|
||||
As Go is using repository path in import paths, it's better to clone aptly repo (not your fork) at default location:
|
||||
#### Create docker container
|
||||
|
||||
mkdir -p ~/go/src/github.com/smira
|
||||
cd ~/go/src/github.com/smira
|
||||
git clone git@github.com:aptly-dev/aptly.git
|
||||
cd aptly
|
||||
To build the development docker image, run:
|
||||
```
|
||||
make docker-image
|
||||
```
|
||||
|
||||
For main repo under your GitHub user and add it as another Git remote:
|
||||
#### Build aptly
|
||||
|
||||
git remote add <user> git@github.com:<user>/aptly.git
|
||||
To build the aptly in the development docker container, run:
|
||||
```
|
||||
make docker-build
|
||||
```
|
||||
|
||||
That way you can continue to build project as is (you don't need to adjust import paths), but you would need
|
||||
to specify your remote name when pushing branches:
|
||||
#### Running aptly commands
|
||||
|
||||
git push <user> <your-branch>
|
||||
To run aptly commands in the development docker container, run:
|
||||
```
|
||||
make docker-shell
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
Example:
|
||||
```
|
||||
$ make docker-shell
|
||||
aptly@b43e8473ef81:/work/src$ aptly version
|
||||
aptly version: 1.5.0+189+g0fc90dff
|
||||
```
|
||||
|
||||
You would need some additional tools and Python virtual environment to run tests and checks, install them with:
|
||||
#### Running unit tests
|
||||
|
||||
make prepare dev system/env
|
||||
In order to run aptly unit tests, enter the following:
|
||||
```
|
||||
make docker-unit-tests
|
||||
```
|
||||
|
||||
This is usually one-time action.
|
||||
#### Running system tests
|
||||
|
||||
### Building
|
||||
In order to run aptly system tests, enter the following:
|
||||
```
|
||||
make docker-system-tests
|
||||
```
|
||||
|
||||
If you want to build aptly binary from your current source tree, run:
|
||||
#### Running golangci-lint
|
||||
|
||||
In order to run aptly unit tests, run:
|
||||
```
|
||||
make docker-lint
|
||||
```
|
||||
|
||||
#### More info
|
||||
|
||||
Run `make help` for more information.
|
||||
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
This section describes local setup to start contributing to aptly.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
Building aptly requires go version 1.22.
|
||||
|
||||
On Debian bookworm with backports enabled, go can be installed with:
|
||||
|
||||
apt install -t bookworm-backports golang-go
|
||||
|
||||
#### Building
|
||||
|
||||
To build aptly, run:
|
||||
|
||||
make build
|
||||
|
||||
Run aptly:
|
||||
|
||||
build/aptly
|
||||
|
||||
To install aptly into `$GOPATH/bin`, run:
|
||||
|
||||
make install
|
||||
|
||||
This would build `aptly` in `$GOPATH/bin`, so depending on your `$PATH`, you should be able to run it immediately with:
|
||||
|
||||
aptly
|
||||
|
||||
Or, if it's not on your path:
|
||||
|
||||
~/go/bin/aptly
|
||||
|
||||
### Unit-tests
|
||||
#### Unit-tests
|
||||
|
||||
aptly has two kinds of tests: unit-tests and functional (system) tests. Functional tests are preferred way to test any
|
||||
feature, but some features are much easier to test with unit-tests (e.g. algorithms, failure scenarios, ...)
|
||||
@@ -131,7 +187,7 @@ aptly is using standard Go unit-test infrastructure plus [gocheck](http://labix.
|
||||
|
||||
make test
|
||||
|
||||
### Functional Tests
|
||||
#### Functional Tests
|
||||
|
||||
Functional tests are implemented in Python, and they use custom test runner which is similar to Python unit-test
|
||||
runner. Most of the tests start with clean aptly state, run some aptly commands to prepare environment, and finally
|
||||
@@ -178,27 +234,6 @@ There are some packages available under `system/files/` directory which are used
|
||||
this default location. You can run aptly under different user or by using non-default config location with non-default
|
||||
aptly root directory.
|
||||
|
||||
### Style Checks
|
||||
|
||||
Style checks could be run with:
|
||||
|
||||
make check
|
||||
|
||||
aptly is using [gometalinter](https://github.com/alecthomas/gometalinter) to run style checks on Go code. Configuration
|
||||
for the linter could be found in [linter.json](linter.json) file. Running linters might take considerable amount of time
|
||||
unfortunately, but usually warning reported by linters hint at real code issues.
|
||||
|
||||
Python code (system tests) are linted with [flake8 tool](https://pypi.python.org/pypi/flake8).
|
||||
|
||||
### Vendored Code
|
||||
|
||||
aptly is using Go vendoring for all the libraries aptly depends upon. `vendor/` directory is checked into the source
|
||||
repository to avoid any problems if source repositories go away. Go build process will automatically prefer vendored
|
||||
packages over packages in `$GOPATH`.
|
||||
|
||||
If you want to update vendored dependencies or to introduce new dependency, use [dep tool](https://github.com/golang/dep).
|
||||
Usually all you need is `dep ensure` or `dep ensure -update`.
|
||||
|
||||
### man Page
|
||||
|
||||
aptly is using combination of [Go templates](http://godoc.org/text/template) and automatically generated text to build `aptly.1` man page. If either source
|
||||
@@ -216,25 +251,3 @@ Bash and Zsh completion for aptly reside in the same repo under in [completion.d
|
||||
When new option or command is introduced, bash completion should be updated to reflect that change.
|
||||
|
||||
When aptly package is being built, it automatically pulls bash completion and man page into the package.
|
||||
|
||||
## Design
|
||||
|
||||
This section requires future work.
|
||||
|
||||
*TBD*
|
||||
|
||||
### Database
|
||||
|
||||
### Package Pool
|
||||
|
||||
### Package
|
||||
|
||||
### PackageList, PackageRefList
|
||||
|
||||
### LocalRepo, RemoteRepo, Snapshot
|
||||
|
||||
### PublishedRepository
|
||||
|
||||
### Context
|
||||
|
||||
### Collections, CollectionFactory
|
||||
|
||||
Generated
-290
@@ -1,290 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/AlekSi/pointer"
|
||||
packages = ["."]
|
||||
revision = "08a25bac605b3fcb6cc27f3917b2c2c87451963d"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/DisposaBoy/JsonConfigReader"
|
||||
packages = ["."]
|
||||
revision = "33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/awalterschulze/gographviz"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"parser",
|
||||
"scanner",
|
||||
"token"
|
||||
]
|
||||
revision = "761fd5fbb34e4c2c138c280395b65b48e4ff5a53"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
"aws",
|
||||
"aws/awserr",
|
||||
"aws/awsutil",
|
||||
"aws/client",
|
||||
"aws/client/metadata",
|
||||
"aws/corehandlers",
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
"private/protocol/restxml",
|
||||
"private/protocol/xml/xmlutil",
|
||||
"service/s3",
|
||||
"service/sts"
|
||||
]
|
||||
revision = "a72204b9bf8d48230ee0fe8995613b394c66f2da"
|
||||
version = "v1.13.31"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cheggaaa/pb"
|
||||
packages = ["."]
|
||||
revision = "cdf719fac0dd208251aa828e687c2d5802053b51"
|
||||
version = "v1.0.10"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gin-contrib/sse"
|
||||
packages = ["."]
|
||||
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gin-gonic/gin"
|
||||
packages = [
|
||||
".",
|
||||
"binding",
|
||||
"render"
|
||||
]
|
||||
revision = "d459835d2b077e44f7c9b453505ee29881d5d12d"
|
||||
version = "v1.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-ini/ini"
|
||||
packages = ["."]
|
||||
revision = "1730955e3146956d6a087861380f9b4667ed5071"
|
||||
version = "v1.26.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "130e6b02ab059e7b717a096f397c5b60111cae74"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/snappy"
|
||||
packages = ["."]
|
||||
revision = "553a641470496b2327abcac10b36396bd98e45c9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/h2non/filetype"
|
||||
packages = ["matchers"]
|
||||
revision = "cc14fdc9ca0e4c2bafad7458f6ff79fd3947cfbb"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jlaffaye/ftp"
|
||||
packages = ["."]
|
||||
revision = "2403248fa8cc9f7909862627aa7337f13f8e0bf1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
revision = "0b12d6b5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-shellwords"
|
||||
packages = ["."]
|
||||
revision = "005a0944d84452842197c2108bd9168ced206f78"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mkrautz/goar"
|
||||
packages = ["."]
|
||||
revision = "282caa8bd9daba480b51f1d5a988714913b97aad"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mxk/go-flowrate"
|
||||
packages = ["flowrate"]
|
||||
revision = "cca7078d478f8520f85629ad7c68962d31ed7682"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ncw/swift"
|
||||
packages = [
|
||||
".",
|
||||
"swifttest"
|
||||
]
|
||||
revision = "8e9b10220613abdbc2896808ee6b43e411a4fa6c"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/commander"
|
||||
packages = ["."]
|
||||
revision = "f408b00e68d5d6e21b9f18bd310978dafc604e47"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/flag"
|
||||
packages = ["."]
|
||||
revision = "695ea5e84e76dea7c8656e43c384e54b32aa1b2a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-aws-auth"
|
||||
packages = ["."]
|
||||
revision = "0070896e9d7f4f9f2d558532b2d896ce2239992a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-ftp-protocol"
|
||||
packages = ["protocol"]
|
||||
revision = "066b75c2b70dca7ae10b1b88b47534a3c31ccfaa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-uuid"
|
||||
packages = ["uuid"]
|
||||
revision = "ed3ca8a15a931b141440a7e98e4f716eec255f7d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-xz"
|
||||
packages = ["."]
|
||||
revision = "0c531f070014e218b21f3cfca801cc992d52726d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/lzma"
|
||||
packages = ["."]
|
||||
revision = "7f0af6269940baa2c938fabe73e0d7ba41205683"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/syndtr/goleveldb"
|
||||
packages = [
|
||||
"leveldb",
|
||||
"leveldb/cache",
|
||||
"leveldb/comparer",
|
||||
"leveldb/errors",
|
||||
"leveldb/filter",
|
||||
"leveldb/iterator",
|
||||
"leveldb/journal",
|
||||
"leveldb/memdb",
|
||||
"leveldb/opt",
|
||||
"leveldb/storage",
|
||||
"leveldb/table",
|
||||
"leveldb/util"
|
||||
]
|
||||
revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
revision = "71c2886f5a673a35f909803f38ece5810165097b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/wsxiaoys/terminal"
|
||||
packages = ["color"]
|
||||
revision = "0940f3fc43a0ed42d04916b1c04578462c650b09"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cast5",
|
||||
"openpgp",
|
||||
"openpgp/armor",
|
||||
"openpgp/clearsign",
|
||||
"openpgp/elgamal",
|
||||
"openpgp/errors",
|
||||
"openpgp/packet",
|
||||
"openpgp/s2k",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "b2aa35443fbc700ab74c586ae79b81c171851023"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "1d206c9fa8975fb4cf00df1dc8bf3283dc24ba0e"
|
||||
|
||||
[[projects]]
|
||||
branch = "v1"
|
||||
name = "gopkg.in/check.v1"
|
||||
packages = ["."]
|
||||
revision = "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/go-playground/validator.v8"
|
||||
packages = ["."]
|
||||
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
||||
version = "v8.18.2"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/h2non/filetype.v1"
|
||||
packages = ["types"]
|
||||
revision = "3093b8ebec6efb56ac813238b8beab4ed4eaac6a"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "5ab2b384766e62be84d3941971a1d8e99c637f80a2cb1482b3d9704c668b549f"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/mkrautz/goar"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-uuid"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/smira/go-xz"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/ugorji/go"
|
||||
revision = "71c2886f5a673a35f909803f38ece5810165097b"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
|
||||
[[override]]
|
||||
branch = "v1"
|
||||
name = "gopkg.in/check.v1"
|
||||
@@ -1,74 +1,244 @@
|
||||
GOVERSION=$(shell go version | awk '{print $$3;}')
|
||||
VERSION=$(shell git describe --tags | sed 's@^v@@' | sed 's@-@+@g')
|
||||
PACKAGES=context database deb files gpg http query swift s3 utils
|
||||
PYTHON?=python
|
||||
TESTS?=
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
VERSION=$(shell make -s version)
|
||||
PYTHON?=python3
|
||||
BINPATH?=$(GOPATH)/bin
|
||||
RUN_LONG_TESTS?=yes
|
||||
GOLANGCI_LINT_VERSION=v2.0.2 # version supporting go 1.24
|
||||
COVERAGE_DIR?=$(shell mktemp -d)
|
||||
GOOS=$(shell go env GOHOSTOS)
|
||||
GOARCH=$(shell go env GOHOSTARCH)
|
||||
|
||||
GO_1_10_AND_HIGHER=$(shell (printf '%s\n' go1.10 $(GOVERSION) | sort -cV >/dev/null 2>&1) && echo "yes")
|
||||
export PODMAN_USERNS = keep-id
|
||||
DOCKER_RUN = docker run --security-opt label=disable --user 0:0 --rm -v ${PWD}:/work/src
|
||||
|
||||
all: test check system-test
|
||||
# Setting TZ for certificates
|
||||
export TZ=UTC
|
||||
# Unit Tests and some sysmte tests rely on expired certificates, turn back the time
|
||||
export TEST_FAKETIME := 2025-01-02 03:04:05
|
||||
|
||||
prepare:
|
||||
go get -u github.com/alecthomas/gometalinter
|
||||
gometalinter --install
|
||||
|
||||
dev:
|
||||
go get -u github.com/golang/dep/...
|
||||
go get -u github.com/laher/goxc
|
||||
|
||||
check: system/env
|
||||
ifeq ($(RUN_LONG_TESTS), yes)
|
||||
if [ -x travis_wait ]; then \
|
||||
travis_wait gometalinter --config=linter.json ./...; \
|
||||
else \
|
||||
gometalinter --config=linter.json ./...; \
|
||||
fi
|
||||
. system/env/bin/activate && flake8 --max-line-length=200 --exclude=system/env/ system/
|
||||
# run with 'COVERAGE_SKIP=1' to skip coverage checks during system tests
|
||||
ifeq ($(COVERAGE_SKIP),1)
|
||||
COVERAGE_ARG_BUILD :=
|
||||
COVERAGE_ARG_TEST := --coverage-skip
|
||||
else
|
||||
COVERAGE_ARG_BUILD := -coverpkg="./..."
|
||||
COVERAGE_ARG_TEST := --coverage-dir $(COVERAGE_DIR)
|
||||
endif
|
||||
|
||||
# export CAPUTRE=1 for regenrating test gold files
|
||||
ifeq ($(CAPTURE),1)
|
||||
CAPTURE_ARG := --capture
|
||||
endif
|
||||
|
||||
help: ## Print this help
|
||||
@grep -E '^[a-zA-Z][a-zA-Z0-9_-]*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
prepare: ## Install go module dependencies
|
||||
# Prepare go modules
|
||||
go mod verify
|
||||
go mod tidy -v
|
||||
# Generate VERSION file
|
||||
go generate
|
||||
|
||||
releasetype: # Print release type: ci (on any branch/commit), release (on a tag)
|
||||
@reltype=ci ; \
|
||||
gitbranch=`git rev-parse --abbrev-ref HEAD` ; \
|
||||
if [ "$$gitbranch" = "HEAD" ] && [ "$$FORCE_CI" != "true" ]; then \
|
||||
gittag=`git describe --tags --exact-match 2>/dev/null` ;\
|
||||
if echo "$$gittag" | grep -q '^v[0-9]'; then \
|
||||
reltype=release ; \
|
||||
fi ; \
|
||||
fi ; \
|
||||
echo $$reltype
|
||||
|
||||
version: ## Print aptly version
|
||||
@ci="" ; \
|
||||
if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
ci=`TZ=UTC git show -s --format='+%cd.%h' --date=format-local:'%Y%m%d%H%M%S'`; \
|
||||
fi ; \
|
||||
if which dpkg-parsechangelog > /dev/null 2>&1; then \
|
||||
echo `dpkg-parsechangelog -S Version`$$ci; \
|
||||
else \
|
||||
echo `grep ^aptly -m1 debian/changelog | sed 's/.*(\([^)]\+\)).*/\1/'`$$ci ; \
|
||||
fi
|
||||
|
||||
swagger-install:
|
||||
# Install swag
|
||||
@test -f $(BINPATH)/swag || GOOS= GOARCH= go install github.com/swaggo/swag/cmd/swag@latest
|
||||
# Generate swagger.conf
|
||||
cp docs/swagger.conf.tpl docs/swagger.conf
|
||||
echo "// @version $(VERSION)" >> docs/swagger.conf
|
||||
|
||||
azurite-start:
|
||||
azurite -l /tmp/aptly-azurite > ~/.azurite.log 2>&1 & \
|
||||
echo $$! > ~/.azurite.pid
|
||||
|
||||
azurite-stop:
|
||||
@kill `cat ~/.azurite.pid`
|
||||
|
||||
swagger: swagger-install
|
||||
# Generate swagger docs
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --propertyStrategy pascalcase --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
|
||||
etcd-install:
|
||||
# Install etcd
|
||||
test -d /tmp/aptly-etcd || system/t13_etcd/install-etcd.sh
|
||||
|
||||
flake8: ## run flake8 on system test python files
|
||||
flake8 system/
|
||||
|
||||
lint: prepare
|
||||
# Install golangci-lint
|
||||
@test -f $(BINPATH)/golangci-lint || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
# Running lint
|
||||
@NO_COLOR=true PATH=$(BINPATH)/:$(PATH) golangci-lint run --max-issues-per-linter=0 --max-same-issues=0
|
||||
|
||||
|
||||
build: prepare swagger ## Build aptly
|
||||
go build -o build/aptly
|
||||
|
||||
install:
|
||||
go install -v -ldflags "-X main.Version=$(VERSION)"
|
||||
@echo "\e[33m\e[1mBuilding aptly ...\e[0m"
|
||||
# go generate
|
||||
@go generate
|
||||
# go install -v
|
||||
@out=`mktemp`; if ! go install -v > $$out 2>&1; then cat $$out; rm -f $$out; echo "\nBuild failed\n"; exit 1; else rm -f $$out; fi
|
||||
|
||||
system/env: system/requirements.txt
|
||||
ifeq ($(RUN_LONG_TESTS), yes)
|
||||
rm -rf system/env
|
||||
virtualenv system/env
|
||||
system/env/bin/pip install -r system/requirements.txt
|
||||
endif
|
||||
test: prepare swagger etcd-install ## Run unit tests (add TEST=regex to specify which tests to run)
|
||||
@echo "\e[33m\e[1mStarting etcd ...\e[0m"
|
||||
@mkdir -p /tmp/aptly-etcd-data; system/t13_etcd/start-etcd.sh > /tmp/aptly-etcd-data/etcd.log 2>&1 &
|
||||
@echo "\e[33m\e[1mRunning go test ...\e[0m"
|
||||
faketime "$(TEST_FAKETIME)" go test -v ./... -gocheck.v=true -check.f "$(TEST)" -coverprofile=unit.out; echo $$? > .unit-test.ret
|
||||
@echo "\e[33m\e[1mStopping etcd ...\e[0m"
|
||||
@pid=`cat /tmp/etcd.pid`; kill $$pid
|
||||
@rm -f /tmp/aptly-etcd-data/etcd.log
|
||||
@ret=`cat .unit-test.ret`; if [ "$$ret" = "0" ]; then echo "\n\e[32m\e[1mUnit Tests SUCCESSFUL\e[0m"; else echo "\n\e[31m\e[1mUnit Tests FAILED\e[0m"; fi; rm -f .unit-test.ret; exit $$ret
|
||||
|
||||
system-test: install system/env
|
||||
ifeq ($(RUN_LONG_TESTS), yes)
|
||||
system-test: prepare swagger etcd-install ## Run system tests
|
||||
# build coverage binary
|
||||
go test -v $(COVERAGE_ARG_BUILD) -c -tags testruncli
|
||||
# Download fixture-db, fixture-pool, etcd.db
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
PATH=$(BINPATH)/:$(PATH) && . system/env/bin/activate && APTLY_VERSION=$(VERSION) $(PYTHON) system/run.py --long $(TESTS)
|
||||
endif
|
||||
test -f ~/etcd.db || (curl -o ~/etcd.db.xz http://repo.aptly.info/system-tests/etcd.db.xz && xz -d ~/etcd.db.xz)
|
||||
# Run system tests
|
||||
PATH=$(BINPATH)/:$(PATH) FORCE_COLOR=1 $(PYTHON) system/run.py --long $(COVERAGE_ARG_TEST) $(CAPTURE_ARG) $(TEST)
|
||||
|
||||
test:
|
||||
ifeq ($(GO_1_10_AND_HIGHER), yes)
|
||||
go test -v ./... -gocheck.v=true -race -coverprofile=coverage.txt -covermode=atomic
|
||||
else
|
||||
go test -v `go list ./... | grep -v vendor/` -gocheck.v=true
|
||||
endif
|
||||
bench:
|
||||
@echo "\e[33m\e[1mRunning benchmark ...\e[0m"
|
||||
go test -v ./deb -run=nothing -bench=. -benchmem
|
||||
|
||||
serve: prepare swagger-install ## Run development server (auto recompiling)
|
||||
test -f $(BINPATH)/air || go install github.com/air-verse/air@v1.52.3
|
||||
cp debian/aptly.conf ~/.aptly.conf
|
||||
sed -i /enable_swagger_endpoint/s/false/true/ ~/.aptly.conf
|
||||
sed -i /enable_metrics_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --propertyStrategy pascalcase --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
|
||||
dpkg: prepare swagger ## Build debian packages
|
||||
@test -n "$(DEBARCH)" || (echo "please define DEBARCH"; exit 1)
|
||||
# set debian version
|
||||
@if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
echo CI Build, setting version... ; \
|
||||
test ! -f debian/changelog.dpkg-bak || mv debian/changelog.dpkg-bak debian/changelog ; \
|
||||
cp debian/changelog debian/changelog.dpkg-bak ; \
|
||||
DEBEMAIL="CI <ci@aptly.info>" dch -v `make -s version` "CI build" ; \
|
||||
fi
|
||||
# clean
|
||||
rm -rf obj-i686-linux-gnu obj-arm-linux-gnueabihf obj-aarch64-linux-gnu obj-x86_64-linux-gnu
|
||||
# Run dpkg-buildpackage
|
||||
@buildtype="any" ; \
|
||||
if [ "$(DEBARCH)" = "amd64" ]; then \
|
||||
buildtype="any,all" ; \
|
||||
fi ; \
|
||||
echo "\e[33m\e[1mBuilding: $$buildtype\e[0m" ; \
|
||||
cmd="dpkg-buildpackage -us -uc --build=$$buildtype -d --host-arch=$(DEBARCH)" ; \
|
||||
echo "$$cmd" ; \
|
||||
$$cmd
|
||||
lintian ../*_$(DEBARCH).changes || true
|
||||
# cleanup
|
||||
@test ! -f debian/changelog.dpkg-bak || mv debian/changelog.dpkg-bak debian/changelog; \
|
||||
mkdir -p build && mv ../*.deb build/ ; \
|
||||
cd build && ls -l *.deb
|
||||
|
||||
binaries: prepare swagger ## Build binary releases (FreeBSD, macOS, Linux generic)
|
||||
# build aptly
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o build/tmp/aptly -ldflags='-extldflags=-static'
|
||||
# install
|
||||
@mkdir -p build/tmp/man build/tmp/completion/bash_completion.d build/tmp/completion/zsh/vendor-completions
|
||||
@cp man/aptly.1 build/tmp/man/
|
||||
@cp completion.d/aptly build/tmp/completion/bash_completion.d/
|
||||
@cp completion.d/_aptly build/tmp/completion/zsh/vendor-completions/
|
||||
@cp README.rst LICENSE AUTHORS build/tmp/
|
||||
@gzip -f build/tmp/man/aptly.1
|
||||
@path="aptly_$(VERSION)_$(GOOS)_$(GOARCH)"; \
|
||||
rm -rf "build/$$path"; \
|
||||
mv build/tmp build/"$$path"; \
|
||||
rm -rf build/tmp; \
|
||||
cd build; \
|
||||
zip -r "$$path".zip "$$path" > /dev/null \
|
||||
&& echo "Built build/$${path}.zip"; \
|
||||
rm -rf "$$path"
|
||||
|
||||
docker-image: ## Build aptly-dev docker image
|
||||
@docker build -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
docker-image-no-cache: ## Build aptly-dev docker image (no cache)
|
||||
@docker build --no-cache -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
docker-build: ## Build aptly in docker container
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper build
|
||||
|
||||
docker-shell: ## Run aptly and other commands in docker container
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 aptly-dev /work/src/system/docker-wrapper || true
|
||||
|
||||
docker-deb: ## Build debian packages in docker container
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
|
||||
docker-unit-tests: ## Run unit tests in docker container (add TEST=regex to specify which tests to run)
|
||||
$(DOCKER_RUN) -t --tmpfs /smallfs:rw,size=1m aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
test TEST=$(TEST) \
|
||||
azurite-stop
|
||||
|
||||
docker-system-test: ## Run system tests in docker container (add TEST=t04_mirror or TEST=UpdateMirror26Test to run only specific tests)
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
|
||||
AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
|
||||
system-test TEST=$(TEST) CAPTURE=$(CAPTURE) COVERAGE_SKIP=$(COVERAGE_SKIP) \
|
||||
azurite-stop
|
||||
|
||||
docker-serve: ## Run development server (auto recompiling) on http://localhost:3142
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 -v /tmp/cache-go-aptly:/var/lib/aptly/.cache/go-build aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
|
||||
docker-lint: ## Run golangci-lint in docker container
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper lint
|
||||
|
||||
docker-binaries: ## Build binary releases (FreeBSD, macOS, Linux generic) in docker container
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper binaries
|
||||
|
||||
docker-man: ## Create man page in docker container
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper man
|
||||
|
||||
mem.png: mem.dat mem.gp
|
||||
gnuplot mem.gp
|
||||
open mem.png
|
||||
|
||||
goxc:
|
||||
rm -rf root/
|
||||
mkdir -p root/usr/share/man/man1/ root/etc/bash_completion.d/ root/usr/share/zsh/vendor-completions/
|
||||
cp man/aptly.1 root/usr/share/man/man1
|
||||
cp completion.d/aptly root/etc/bash_completion.d/
|
||||
cp completion.d/_aptly root/usr/share/zsh/vendor-completions/
|
||||
gzip root/usr/share/man/man1/aptly.1
|
||||
goxc -pv=$(VERSION) -max-processors=4 $(GOXC_OPTS)
|
||||
|
||||
man:
|
||||
man: ## Create man pages
|
||||
make -C man
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
clean: ## remove local build and module cache
|
||||
# Clean all generated and build files
|
||||
test ! -e .go || find .go/ -type d ! -perm -u=w -exec chmod u+w {} \;
|
||||
rm -rf .go/
|
||||
rm -rf build/ obj-*-linux-gnu* tmp/
|
||||
rm -f unit.out aptly.test VERSION docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
find system/ -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true
|
||||
|
||||
.PHONY: man version
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-tests docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
|
||||
+62
-45
@@ -1,18 +1,17 @@
|
||||
=====
|
||||
aptly
|
||||
=====
|
||||
|
||||
.. image:: https://api.travis-ci.org/aptly-dev/aptly.svg?branch=master
|
||||
:target: https://travis-ci.org/aptly-dev/aptly
|
||||
.. image:: https://github.com/aptly-dev/aptly/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/aptly-dev/aptly/actions
|
||||
|
||||
.. image:: https://codecov.io/gh/aptly-dev/aptly/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/aptly-dev/aptly
|
||||
:target: https://codecov.io/gh/aptly-dev/aptly
|
||||
|
||||
.. image:: https://badges.gitter.im/Join Chat.svg
|
||||
:target: https://gitter.im/aptly-dev/aptly?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
:target: https://matrix.to/#/#aptly:gitter.im
|
||||
|
||||
.. image:: http://goreportcard.com/badge/aptly-dev/aptly
|
||||
:target: http://goreportcard.com/report/aptly-dev/aptly
|
||||
.. image:: https://goreportcard.com/badge/github.com/aptly-dev/aptly
|
||||
:target: https://goreportcard.com/report/aptly-dev/aptly
|
||||
|
||||
aptly
|
||||
=====
|
||||
|
||||
Aptly is a swiss army knife for Debian repository management.
|
||||
|
||||
@@ -20,9 +19,9 @@ Aptly is a swiss army knife for Debian repository management.
|
||||
:target: http://www.aptly.info/
|
||||
|
||||
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support please use
|
||||
mailing list `aptly-discuss <https://groups.google.com/forum/#!forum/aptly-discuss>`_.
|
||||
open `issues <https://github.com/aptly-dev/aptly/issues>`_ or `discussions <https://github.com/aptly-dev/aptly/discussions>`_.
|
||||
|
||||
Aptly features: ("+" means planned features)
|
||||
Aptly features:
|
||||
|
||||
* make mirrors of remote Debian/Ubuntu repositories, limiting by components/architectures
|
||||
* take snapshots of mirrors at any point in time, fixing state of repository at some moment of time
|
||||
@@ -32,54 +31,70 @@ Aptly features: ("+" means planned features)
|
||||
* filter repository by search query, pulling dependencies when required
|
||||
* publish self-made packages as Debian repositories
|
||||
* REST API for remote access
|
||||
* mirror repositories "as-is" (without resigning with user's key) (+)
|
||||
* support for yum repositories (+)
|
||||
|
||||
Current limitations:
|
||||
Any contributions are welcome! Please see `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
|
||||
* translations are not supported yet
|
||||
Installation
|
||||
=============
|
||||
|
||||
Download
|
||||
--------
|
||||
Aptly can be installed on several operating systems.
|
||||
|
||||
To install aptly on Debian/Ubuntu, add new repository to ``/etc/apt/sources.list``::
|
||||
Debian / Ubuntu
|
||||
----------------
|
||||
|
||||
deb http://repo.aptly.info/ squeeze main
|
||||
Aptly is provided in the following debian packages:
|
||||
|
||||
And import key that is used to sign the release::
|
||||
* **aptly**: Includes the main Aptly binary, man pages, and shell completions
|
||||
* **aptly-api**: A systemd service for the REST API, using the global /etc/aptly.conf
|
||||
* **aptly-dbg**: Debug symbols for troubleshooting
|
||||
|
||||
$ apt-key adv --keyserver pool.sks-keyservers.net --recv-keys ED75B5A4483DA07C
|
||||
The packages can be installed on official `Debian <https://packages.debian.org/search?keywords=aptly>`_ and `Ubuntu <https://packages.ubuntu.com/search?keywords=aptly>`_ distributions.
|
||||
|
||||
After that you can install aptly as any other software package::
|
||||
Upstream Debian Packages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
$ apt-get update
|
||||
$ apt-get install aptly
|
||||
If a newer version (not available in Debian/Ubuntu) of aptly is required, upstream debian packages (built from git tags) can be installed as follows:
|
||||
|
||||
Don't worry about squeeze part in repo name: aptly package should work on Debian squeeze+,
|
||||
Ubuntu 10.0+. Package contains aptly binary, man page and bash completion.
|
||||
Install the following APT key (as root)::
|
||||
|
||||
If you would like to use nightly builds (unstable), please use following repository::
|
||||
wget -O /etc/apt/keyrings/aptly.asc https://www.aptly.info/pubkey.txt
|
||||
|
||||
deb http://repo.aptly.info/ nightly main
|
||||
Define Release APT sources in ``/etc/apt/sources.list.d/aptly.list``::
|
||||
|
||||
Binary executables (depends almost only on libc) are available for download from `Bintray <http://dl.bintray.com/smira/aptly/>`_.
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/release DIST main
|
||||
|
||||
If you have Go environment set up, you can build aptly from source by running (go 1.8+ required)::
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
mkdir -p $GOPATH/src/github.com/aptly-dev/aptly
|
||||
git clone https://github.com/aptly-dev/aptly $GOPATH/src/github.com/aptly-dev/aptly
|
||||
cd $GOPATH/src/github.com/aptly-dev/aptly
|
||||
make install
|
||||
Install aptly packages::
|
||||
|
||||
Binary would be installed to ```$GOPATH/bin/aptly``.
|
||||
apt-get update
|
||||
apt-get install aptly
|
||||
apt-get install aptly-api # REST API systemd service
|
||||
|
||||
Contributing
|
||||
------------
|
||||
CI Builds
|
||||
~~~~~~~~~~
|
||||
|
||||
Please follow detailed documentation in `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
For testing new features or bugfixes, recent builds are available as CI builds (built from master, may be unstable!) and can be installed as follows:
|
||||
|
||||
Define CI APT sources in ``/etc/apt/sources.list.d/aptly-ci.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/ci DIST main
|
||||
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Note: same gpg key is used as for the Upstream Debian Packages.
|
||||
|
||||
Other Operating Systems
|
||||
------------------------
|
||||
|
||||
Binary executables (depends almost only on libc) are available on `GitHub Releases <https://github.com/aptly-dev/aptly/releases>`_ for:
|
||||
|
||||
- macOS / darwin (amd64, arm64)
|
||||
- FreeBSD (amd64, arm64, 386, arm)
|
||||
- Generic Linux (amd64, arm64, 386, arm)
|
||||
|
||||
Integrations
|
||||
------------
|
||||
=============
|
||||
|
||||
Vagrant:
|
||||
|
||||
@@ -90,16 +105,14 @@ Vagrant:
|
||||
Docker:
|
||||
|
||||
- `Docker container <https://github.com/mikepurvis/aptly-docker>`_ with aptly inside by Mike Purvis
|
||||
- `Docker container <https://github.com/bryanhong/docker-aptly>`_ with aptly and nginx by Bryan Hong
|
||||
- `Docker container <https://github.com/urpylka/docker-aptly>`_ with aptly and nginx by Artem Smirnov
|
||||
|
||||
With configuration management systems:
|
||||
|
||||
- `Chef cookbook <https://github.com/hw-cookbooks/aptly>`_ by Aaron Baer
|
||||
(Heavy Water Operations, LLC)
|
||||
- `Puppet module <https://github.com/alphagov/puppet-aptly>`_ by
|
||||
Government Digital Services
|
||||
- `Puppet module <https://github.com/tubemogul/puppet-aptly>`_ by
|
||||
TubeMogul
|
||||
- `Puppet module <https://github.com/voxpupuli/puppet-aptly>`_ by
|
||||
Vox Pupuli
|
||||
- `SaltStack Formula <https://github.com/saltstack-formulas/aptly-formula>`_ by
|
||||
Forrest Alvarez and Brian Jackson
|
||||
- `Ansible role <https://github.com/aioue/ansible-role-aptly>`_ by Tom Paine
|
||||
@@ -116,3 +129,7 @@ GUI for aptly API:
|
||||
Scala sbt:
|
||||
|
||||
- `sbt aptly plugin <https://github.com/amalakar/sbt-aptly>`_ by Arup Malakar
|
||||
|
||||
Molior:
|
||||
|
||||
- `Molior Debian Build System <https://github.com/molior-dbs/molior>`_ by André Roth
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Creating a Release
|
||||
|
||||
- create branch release/1.x.y
|
||||
- update debian/changelog
|
||||
- create PR, merge when approved
|
||||
- on updated master, create release:
|
||||
```
|
||||
version=$(dpkg-parsechangelog -S Version)
|
||||
echo Releasing prod version $version
|
||||
git tag -a v$version -m 'aptly: release $version'
|
||||
git push origin v$version master
|
||||
```
|
||||
- run swagger locally (`make docker-serve`)
|
||||
- copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json
|
||||
- add new version to select tag in content/doc/api/swagger.md line 48
|
||||
- update version in content/download.md
|
||||
- push commit to master
|
||||
- create release announcement on https://github.com/aptly-dev/aptly/discussions
|
||||
+218
-51
@@ -3,13 +3,18 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Lock order acquisition (canonical):
|
||||
@@ -18,9 +23,69 @@ import (
|
||||
// 3. SnapshotCollection
|
||||
// 4. PublishedRepoCollection
|
||||
|
||||
// GET /api/version
|
||||
type aptlyVersion struct {
|
||||
// Aptly Version
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// @Summary Aptly Version
|
||||
// @Description **Get aptly version**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/version
|
||||
// @Description {"Version":"0.9~dev"}
|
||||
// @Description ```
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyVersion
|
||||
// @Router /api/version [get]
|
||||
func apiVersion(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"Version": aptly.Version})
|
||||
version := aptlyVersion{
|
||||
Version: aptly.Version,
|
||||
}
|
||||
c.JSON(200, version)
|
||||
}
|
||||
|
||||
type aptlyStatus struct {
|
||||
// Aptly Status
|
||||
Status string `json:"Status" example:"'Aptly is ready', 'Aptly is unavailable', 'Aptly is healthy'"`
|
||||
}
|
||||
|
||||
// @Summary Get Ready State
|
||||
// @Description **Get aptly ready state**
|
||||
// @Description
|
||||
// @Description Return aptly ready state:
|
||||
// @Description - `Aptly is ready` (HTTP 200)
|
||||
// @Description - `Aptly is unavailable` (HTTP 503)
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyStatus "Aptly is ready"
|
||||
// @Failure 503 {object} aptlyStatus "Aptly is unavailable"
|
||||
// @Router /api/ready [get]
|
||||
func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
if isReady == nil || !isReady.Load().(bool) {
|
||||
c.JSON(503, gin.H{"Status": "Aptly is unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Get Health State
|
||||
// @Description **Get aptly health state**
|
||||
// @Description
|
||||
// @Description Return aptly health state:
|
||||
// @Description - `Aptly is healthy` (HTTP 200)
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyStatus
|
||||
// @Router /api/healthy [get]
|
||||
func apiHealthy(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"Status": "Aptly is healthy"})
|
||||
}
|
||||
|
||||
type dbRequestKind int
|
||||
@@ -35,49 +100,14 @@ type dbRequest struct {
|
||||
err chan<- error
|
||||
}
|
||||
|
||||
// Flushes all collections which cache in-memory objects
|
||||
func flushColections() {
|
||||
// lock everything to eliminate in-progress calls
|
||||
r := context.CollectionFactory().RemoteRepoCollection()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
l := context.CollectionFactory().LocalRepoCollection()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
s := context.CollectionFactory().SnapshotCollection()
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
p := context.CollectionFactory().PublishedRepoCollection()
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
// all collections locked, flush them
|
||||
context.CollectionFactory().Flush()
|
||||
}
|
||||
|
||||
// Periodically flushes CollectionFactory to free up memory used by
|
||||
// collections, flushing caches.
|
||||
//
|
||||
// Should be run in goroutine!
|
||||
func cacheFlusher() {
|
||||
ticker := time.Tick(15 * time.Minute)
|
||||
|
||||
for {
|
||||
<-ticker
|
||||
|
||||
flushColections()
|
||||
}
|
||||
}
|
||||
var dbRequests chan dbRequest
|
||||
|
||||
// Acquire database lock and release it when not needed anymore.
|
||||
//
|
||||
// Should be run in a goroutine!
|
||||
func acquireDatabase(requests <-chan dbRequest) {
|
||||
func acquireDatabase() {
|
||||
clients := 0
|
||||
for request := range requests {
|
||||
for request := range dbRequests {
|
||||
var err error
|
||||
|
||||
switch request.kind {
|
||||
@@ -94,7 +124,6 @@ func acquireDatabase(requests <-chan dbRequest) {
|
||||
case releasedb:
|
||||
clients--
|
||||
if clients == 0 {
|
||||
flushColections()
|
||||
err = context.CloseDatabase()
|
||||
} else {
|
||||
err = nil
|
||||
@@ -105,14 +134,111 @@ func acquireDatabase(requests <-chan dbRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Should be called before database access is needed in any api call.
|
||||
// Happens per default for each api call. It is important that you run
|
||||
// runTaskInBackground to run a task which accquire database.
|
||||
// Important do not forget to defer to releaseDatabaseConnection
|
||||
func acquireDatabaseConnection() error {
|
||||
if dbRequests == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{acquiredb, errCh}
|
||||
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// Release database connection when not needed anymore
|
||||
func releaseDatabaseConnection() error {
|
||||
if dbRequests == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{releasedb, errCh}
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// runs tasks in background. Acquires database connection first.
|
||||
func runTaskInBackground(name string, resources []string, proc task.Process) (task.Task, *task.ResourceConflictError) {
|
||||
return context.TaskList().RunTaskInBackground(name, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := acquireDatabaseConnection()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = releaseDatabaseConnection() }()
|
||||
return proc(out, detail)
|
||||
})
|
||||
}
|
||||
|
||||
func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(v) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case int:
|
||||
return v != 0
|
||||
case bool:
|
||||
return v
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func maybeRunTaskInBackground(c *gin.Context, name string, resources []string, proc task.Process) {
|
||||
// Run this task in background if configured globally or per-request
|
||||
background := truthy(c.DefaultQuery("_async", strconv.FormatBool(context.Config().AsyncAPI)))
|
||||
if background {
|
||||
log.Debug().Msg("Executing task asynchronously")
|
||||
task, conflictErr := runTaskInBackground(name, resources, proc)
|
||||
if conflictErr != nil {
|
||||
AbortWithJSONError(c, 409, conflictErr)
|
||||
return
|
||||
}
|
||||
c.JSON(202, task)
|
||||
} else {
|
||||
log.Debug().Msg("Executing task synchronously")
|
||||
task, conflictErr := runTaskInBackground(name, resources, proc)
|
||||
if conflictErr != nil {
|
||||
AbortWithJSONError(c, 409, conflictErr)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for task to finish
|
||||
_, _ = context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
retValue, _ := context.TaskList().GetTaskReturnValueByID(task.ID)
|
||||
err, _ := context.TaskList().GetTaskErrorByID(task.ID)
|
||||
_, _ = context.TaskList().DeleteTaskByID(task.ID)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, retValue.Code, err)
|
||||
return
|
||||
}
|
||||
if retValue != nil {
|
||||
c.JSON(retValue.Code, retValue.Value)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common piece of code to show list of packages,
|
||||
// with searching & details if requested
|
||||
func showPackages(c *gin.Context, reflist *deb.PackageRefList) {
|
||||
func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory *deb.CollectionFactory) {
|
||||
result := []*deb.Package{}
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(reflist, context.CollectionFactory().PackageCollection(), nil)
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
c.AbortWithError(404, err)
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +246,7 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList) {
|
||||
if queryS != "" {
|
||||
q, err := query.Parse(c.Request.URL.Query().Get("q"))
|
||||
if err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,23 +263,59 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList) {
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
c.AbortWithError(400, fmt.Errorf("unable to determine list of architectures, please specify explicitly"))
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to determine list of architectures, please specify explicitly"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
|
||||
list, err = list.Filter([]deb.PackageQuery{q}, withDeps,
|
||||
nil, context.DependencyOptions(), architecturesList)
|
||||
list, err = list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{q},
|
||||
WithDependencies: withDeps,
|
||||
Source: nil,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithError(500, fmt.Errorf("unable to search: %s", err))
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to search: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// filter packages by version
|
||||
if c.Request.URL.Query().Get("maximumVersion") == "1" {
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
versionQ, err := query.Parse(fmt.Sprintf("Name (%s), $Version (<= %s)", p.Name, p.Version))
|
||||
if err != nil {
|
||||
fmt.Println("filter packages by version, query string parse err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
} else {
|
||||
tmpList, err := list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{versionQ},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
if tmpList.Len() > 0 {
|
||||
_ = tmpList.ForEach(func(tp *deb.Package) error {
|
||||
list.Remove(tp)
|
||||
return nil
|
||||
})
|
||||
_ = list.Add(p)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("filter packages by version, filter err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
@@ -163,3 +325,8 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList) {
|
||||
c.JSON(200, list.Strings())
|
||||
}
|
||||
}
|
||||
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) {
|
||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = c.AbortWithError(code, err)
|
||||
}
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type APISuite struct {
|
||||
context *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&APISuite{})
|
||||
|
||||
func createTestConfig() *os.File {
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"enableMetricsEndpoint": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, _ = file.Write(jsonString)
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *APISuite) setupContext() error {
|
||||
aptly.Version = "testVersion"
|
||||
file := createTestConfig()
|
||||
if nil == file {
|
||||
return fmt.Errorf("unable to create the test configuration file")
|
||||
}
|
||||
s.configFile = file
|
||||
|
||||
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", false, "dummy")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "dummy")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
context, err := ctx.NewContext(s.flags)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
s.context = context
|
||||
s.router = Router(context)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpSuite(c *C) {
|
||||
err := s.setupContext()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownSuite(c *C) {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
s.context.Shutdown()
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGinRunsInReleaseMode(c *C) {
|
||||
c.Check(gin.Mode(), Equals, gin.ReleaseMode)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetVersion(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/version", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Version\":\""+aptly.Version+"\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetReadiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/ready", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is ready\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetHealthiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/healthy", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is healthy\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetMetrics(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/metrics", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
b := strings.Replace(response.Body.String(), "\n", "", -1)
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_requests_in_flight gauge.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_requests_total counter.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_request_size_bytes summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_response_size_bytes summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_request_duration_seconds summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_build_info gauge.*")
|
||||
c.Check(b, Matches, ".*aptly_build_info.*version=\"testVersion\".*")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestRepoCreate(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestTruthy(c *C) {
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
c.Check(truthy("false"), Equals, false)
|
||||
c.Check(truthy("0"), Equals, false)
|
||||
c.Check(truthy(false), Equals, false)
|
||||
c.Check(truthy(0), Equals, false)
|
||||
|
||||
c.Check(truthy("y"), Equals, true)
|
||||
c.Check(truthy("yes"), Equals, true)
|
||||
c.Check(truthy("t"), Equals, true)
|
||||
c.Check(truthy("true"), Equals, true)
|
||||
c.Check(truthy("1"), Equals, true)
|
||||
c.Check(truthy(true), Equals, true)
|
||||
c.Check(truthy(1), Equals, true)
|
||||
|
||||
c.Check(truthy(nil), Equals, false)
|
||||
|
||||
c.Check(truthy("foobar"), Equals, true)
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(gin.H{}), Equals, true)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary DB Cleanup
|
||||
// @Description **Cleanup Aptly DB**
|
||||
// @Description Database cleanup removes information about unreferenced packages and deletes files in the package pool that aren’t used by packages anymore.
|
||||
// @Description It is a good idea to run this command after massive deletion of mirrors, snapshots or local repos.
|
||||
// @Tags Database
|
||||
// @Produce json
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 200 {object} string "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/db/cleanup [post]
|
||||
func apiDBCleanup(c *gin.Context) {
|
||||
resources := []string{string(task.AllResourcesKey)}
|
||||
maybeRunTaskInBackground(c, "Clean up db", resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
var err error
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// collect information about referenced packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
|
||||
out.Printf("Loading mirrors, local repos, snapshots and published repos...")
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
existingPackageRefs = existingPackageRefs.Merge(snapshot.RefList(), false, true)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(published *deb.PublishedRepo) error {
|
||||
if published.SourceKind != deb.SourceLocalRepo {
|
||||
return nil
|
||||
}
|
||||
e := collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
for _, component := range published.Components() {
|
||||
existingPackageRefs = existingPackageRefs.Merge(published.RefList(component), false, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ... and compare it to the list of all packages
|
||||
out.Printf("Loading list of all packages...")
|
||||
allPackageRefs := collectionFactory.PackageCollection().AllPackageRefs()
|
||||
|
||||
toDelete := allPackageRefs.Subtract(existingPackageRefs)
|
||||
|
||||
// delete packages that are no longer referenced
|
||||
out.Printf("Deleting unreferenced packages (%d)...", toDelete.Len())
|
||||
|
||||
// database can't err as collection factory already constructed
|
||||
db, _ := context.Database()
|
||||
|
||||
if toDelete.Len() > 0 {
|
||||
batch := db.CreateBatch()
|
||||
_ = toDelete.ForEach(func(ref []byte) error {
|
||||
_ = collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
return nil
|
||||
})
|
||||
|
||||
err = batch.Write()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to write to DB: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// now, build a list of files that should be present in Repository (package pool)
|
||||
out.Printf("Building list of files referenced by packages...")
|
||||
referencedFiles := make([]string, 0, existingPackageRefs.Len())
|
||||
|
||||
err = existingPackageRefs.ForEach(func(key []byte) error {
|
||||
pkg, err2 := collectionFactory.PackageCollection().ByKey(key)
|
||||
if err2 != nil {
|
||||
tail := ""
|
||||
return fmt.Errorf("unable to load package %s: %s%s", string(key), err2, tail)
|
||||
}
|
||||
paths, err2 := pkg.FilepathList(context.PackagePool())
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
referencedFiles = append(referencedFiles, paths...)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(referencedFiles)
|
||||
|
||||
// build a list of files in the package pool
|
||||
out.Printf("Building list of files in package pool...")
|
||||
existingFiles, err := context.PackagePool().FilepathList(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to collect file paths: %s", err)
|
||||
}
|
||||
|
||||
// find files which are in the pool but not referenced by packages
|
||||
filesToDelete := utils.StrSlicesSubstract(existingFiles, referencedFiles)
|
||||
|
||||
// delete files that are no longer referenced
|
||||
out.Printf("Deleting unreferenced files (%d)...", len(filesToDelete))
|
||||
|
||||
countFilesToDelete := len(filesToDelete)
|
||||
taskDetail := struct {
|
||||
TotalNumberOfPackagesToDelete int
|
||||
RemainingNumberOfPackagesToDelete int
|
||||
}{
|
||||
countFilesToDelete, countFilesToDelete,
|
||||
}
|
||||
detail.Store(taskDetail)
|
||||
|
||||
if countFilesToDelete > 0 {
|
||||
var size, totalSize int64
|
||||
for _, file := range filesToDelete {
|
||||
size, err = context.PackagePool().Remove(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDetail.RemainingNumberOfPackagesToDelete--
|
||||
detail.Store(taskDetail)
|
||||
totalSize += size
|
||||
}
|
||||
|
||||
out.Printf("Disk space freed: %s...", utils.HumanBytes(totalSize))
|
||||
}
|
||||
|
||||
out.Printf("Compacting database...")
|
||||
return nil, db.CompactDB()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package api
|
||||
|
||||
type Error struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
+150
-31
@@ -6,10 +6,17 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
|
||||
// In production it calls (*os.File).Sync().
|
||||
var syncFile = func(f *os.File) error { return f.Sync() }
|
||||
|
||||
func verifyPath(path string) bool {
|
||||
path = filepath.Clean(path)
|
||||
for _, part := range strings.Split(path, string(filepath.Separator)) {
|
||||
@@ -24,27 +31,37 @@ func verifyPath(path string) bool {
|
||||
|
||||
func verifyDir(c *gin.Context) bool {
|
||||
if !verifyPath(c.Params.ByName("dir")) {
|
||||
c.AbortWithError(400, fmt.Errorf("wrong dir"))
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong dir"))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GET /files
|
||||
// @Summary List Directories
|
||||
// @Description **Get list of upload directories**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/files
|
||||
// @Description ["aptly-0.9"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of files"
|
||||
// @Router /api/files [get]
|
||||
func apiFilesListDirs(c *gin.Context) {
|
||||
list := []string{}
|
||||
listLock := &sync.Mutex{}
|
||||
|
||||
err := filepath.Walk(context.UploadPath(), func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := walker.Walk(context.UploadPath(), func(path string, info os.FileInfo) error {
|
||||
if path == context.UploadPath() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
listLock.Lock()
|
||||
defer listLock.Unlock()
|
||||
list = append(list, filepath.Base(path))
|
||||
return filepath.SkipDir
|
||||
}
|
||||
@@ -53,76 +70,146 @@ func apiFilesListDirs(c *gin.Context) {
|
||||
})
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
c.AbortWithError(400, err)
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, list)
|
||||
}
|
||||
|
||||
// POST /files/:dir/
|
||||
// @Summary Upload Files
|
||||
// @Description **Upload files to a directory**
|
||||
// @Description
|
||||
// @Description - one or more files can be uploaded
|
||||
// @Description - existing uploaded are overwritten
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X POST -F file=@aptly_0.9~dev+217+ge5d646c_i386.deb http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description ["aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Accept multipart/form-data
|
||||
// @Param dir path string true "Directory to upload files to. Created if does not exist"
|
||||
// @Param files formData file true "Files to upload"
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "list of uploaded files"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [post]
|
||||
func apiFilesUpload(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(context.UploadPath(), c.Params.ByName("dir"))
|
||||
path := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
|
||||
err := os.MkdirAll(path, 0777)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.Request.ParseMultipartForm(10 * 1024 * 1024)
|
||||
if err != nil {
|
||||
c.AbortWithError(400, err)
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
stored := []string{}
|
||||
openFiles := []*os.File{}
|
||||
|
||||
// Write all files first
|
||||
for _, files := range c.Request.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
destPath := filepath.Join(path, filepath.Base(file.Filename))
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
_ = src.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
_ = src.Close()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
_ = dst.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Keep file open for batch sync
|
||||
openFiles = append(openFiles, dst)
|
||||
stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, stored)
|
||||
// Sync all files at once to catch ENOSPC errors
|
||||
for i, dst := range openFiles {
|
||||
err := syncFile(dst)
|
||||
if err != nil {
|
||||
// Close all files
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", stored[i], err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Close all files
|
||||
for _, dst := range openFiles {
|
||||
_ = dst.Close()
|
||||
}
|
||||
|
||||
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
|
||||
c.JSON(200, stored)
|
||||
}
|
||||
|
||||
// GET /files/:dir
|
||||
// @Summary List Files
|
||||
// @Description **Show uploaded files in upload directory**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description ["aptly_0.9~dev+217+ge5d646c_i386.deb"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory to list"
|
||||
// @Success 200 {array} string "Files found in directory"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [get]
|
||||
func apiFilesListFiles(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
list := []string{}
|
||||
root := filepath.Join(context.UploadPath(), c.Params.ByName("dir"))
|
||||
listLock := &sync.Mutex{}
|
||||
root := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
|
||||
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
err := filepath.Walk(root, func(path string, _ os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,6 +218,8 @@ func apiFilesListFiles(c *gin.Context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
listLock.Lock()
|
||||
defer listLock.Unlock()
|
||||
list = append(list, filepath.Base(path))
|
||||
|
||||
return nil
|
||||
@@ -138,9 +227,9 @@ func apiFilesListFiles(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.AbortWithError(404, err)
|
||||
AbortWithJSONError(c, 404, err)
|
||||
} else {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -148,36 +237,66 @@ func apiFilesListFiles(c *gin.Context) {
|
||||
c.JSON(200, list)
|
||||
}
|
||||
|
||||
// DELETE /files/:dir
|
||||
// @Summary Delete Directory
|
||||
// @Description **Delete upload directory and uploaded files within**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description {}
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [delete]
|
||||
func apiFilesDeleteDir(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.RemoveAll(filepath.Join(context.UploadPath(), c.Params.ByName("dir")))
|
||||
err := os.RemoveAll(filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir"))))
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// DELETE /files/:dir/:name
|
||||
// @Summary Delete File
|
||||
// @Description **Delete a uploaded file in upload directory**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb
|
||||
// @Description {}
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory to delete from"
|
||||
// @Param name path string true "File to delete"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir}/{name} [delete]
|
||||
func apiFilesDeleteFile(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
if !verifyPath(c.Params.ByName("name")) {
|
||||
c.AbortWithError(400, fmt.Errorf("wrong file"))
|
||||
dir := utils.SanitizePath(c.Params.ByName("dir"))
|
||||
name := utils.SanitizePath(c.Params.ByName("name"))
|
||||
if !verifyPath(name) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
|
||||
return
|
||||
}
|
||||
|
||||
err := os.Remove(filepath.Join(context.UploadPath(), c.Params.ByName("dir"), c.Params.ByName("name")))
|
||||
err := os.Remove(filepath.Join(context.UploadPath(), dir, name))
|
||||
if err != nil {
|
||||
if err1, ok := err.(*os.PathError); !ok || !os.IsNotExist(err1.Err) {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type FilesUploadDiskFullSuite struct {
|
||||
aptlyContext *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&FilesUploadDiskFullSuite{})
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) SetUpTest(c *C) {
|
||||
aptly.Version = "testVersion"
|
||||
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
c.Assert(err, IsNil)
|
||||
s.configFile = file
|
||||
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"rootDir": c.MkDir(),
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
_, err = file.Write(jsonString)
|
||||
c.Assert(err, IsNil)
|
||||
_ = file.Close()
|
||||
|
||||
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", false, "dummy")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "dummy")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
aptlyContext, err := ctx.NewContext(s.flags)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.aptlyContext = aptlyContext
|
||||
s.router = Router(aptlyContext)
|
||||
context = aptlyContext
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TearDownTest(c *C) {
|
||||
if s.configFile != nil {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
}
|
||||
if s.aptlyContext != nil {
|
||||
s.aptlyContext.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadSuccessWithSync(c *C) {
|
||||
testContent := []byte("test file content for upload")
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "testfile.txt")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = part.Write(testContent)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir", "testfile.txt")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadVerifiesFileIntegrity(c *C) {
|
||||
testContent := bytes.Repeat([]byte("A"), 10000)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "largefile.bin")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = io.Copy(part, bytes.NewReader(testContent))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir2", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir2", "largefile.bin")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(content), Equals, len(testContent))
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadMultipleFilesWithBatchSync(c *C) {
|
||||
testFiles := map[string][]byte{
|
||||
"file1.txt": []byte("content of file 1"),
|
||||
"file2.txt": bytes.Repeat([]byte("B"), 5000),
|
||||
"file3.deb": []byte("debian package content"),
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
for filename, content := range testFiles {
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write(content)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/multitest", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadDir := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "multitest")
|
||||
for filename, expectedContent := range testFiles {
|
||||
uploadedFile := filepath.Join(uploadDir, filename)
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil, Commentf("Failed to read %s", filename))
|
||||
c.Check(content, DeepEquals, expectedContent, Commentf("Content mismatch for %s", filename))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturnsErrorOnSyncFailure(c *C) {
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(f *os.File) error {
|
||||
if filepath.Base(f.Name()) == "syncfail.txt" {
|
||||
return syscall.ENOSPC
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part1, err := writer.CreateFormFile("file", "ok.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part1.Write([]byte("ok"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
part2, err := writer.CreateFormFile("file", "syncfail.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part2.Write([]byte("will fail on sync"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/syncfaildir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
c.Check(bytes.Contains(w.Body.Bytes(), []byte("error syncing file")), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestVerifyPath(c *C) {
|
||||
c.Check(verifyPath("a/b/c"), Equals, true)
|
||||
c.Check(verifyPath("../x"), Equals, false)
|
||||
c.Check(verifyPath("./x"), Equals, true)
|
||||
c.Check(verifyPath(".."), Equals, false)
|
||||
c.Check(verifyPath("."), Equals, false)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyWhenUploadMissing(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsReturnsDirectories(c *C) {
|
||||
uploadRoot := s.aptlyContext.UploadPath()
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d1"), 0777), IsNil)
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d2"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(uploadRoot, "rootfile"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "d1"), Equals, true)
|
||||
c.Check(strings.Contains(body, "d2"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesNotFound(c *C) {
|
||||
req, err := http.NewRequest("GET", "/api/files/does-not-exist", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturnsFiles(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dir")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "b.txt"), []byte("b"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/dir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "a.txt"), Equals, true)
|
||||
c.Check(strings.Contains(body, "b.txt"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirRemovesDirectory(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(base)
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileRemovesFile(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel2")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel2/a.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(filepath.Join(base, "a.txt"))
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileNotFoundStillOk(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel3")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel3/nope.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidDir(c *C) {
|
||||
req, err := http.NewRequest("DELETE", "/api/files/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidFileName(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirx")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirx/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyIfUploadPathIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturns500OnPermissionError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "noperms")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.Chmod(base, 0), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/noperms", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileReturns500OnNonNotExistError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirisfile")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
subdir := filepath.Join(base, "subdir")
|
||||
c.Assert(os.MkdirAll(subdir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(subdir, "x"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirisfile/subdir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadBadMultipartReturns400(c *C) {
|
||||
req, err := http.NewRequest("POST", "/api/files/badmultipart", bytes.NewBufferString("not multipart"))
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=missing")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadRejectsInvalidDir(c *C) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/..", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500IfUploadRootIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnFileOpenFailure(c *C) {
|
||||
// Pre-populate MultipartForm to inject a FileHeader that fails on Open().
|
||||
form := &multipart.Form{
|
||||
File: map[string][]*multipart.FileHeader{
|
||||
"file": {{Filename: "broken.bin"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/openfaildir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
req.MultipartForm = form
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnCreateFailure(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "readonly")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.Chmod(base, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/readonly", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirReturns500OnRemoveFailure(c *C) {
|
||||
parent := s.aptlyContext.UploadPath()
|
||||
base := filepath.Join(parent, "cantremove")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
c.Assert(os.Chmod(parent, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(parent, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/cantremove", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type gpgKeyInfo struct {
|
||||
// 16-character key ID (short form)
|
||||
KeyID string `json:"KeyID" example:"8B48AD6246925553"`
|
||||
// Full fingerprint
|
||||
Fingerprint string `json:"Fingerprint" example:"D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0"`
|
||||
// Key validity (u=unknown, f=fulltrust, m=marginal, n=never)
|
||||
Validity string `json:"Validity" example:"u"`
|
||||
// User ID(s) associated with this key
|
||||
UserIDs []string `json:"UserIDs" example:"John Doe <john@example.com>"`
|
||||
// Creation date (Unix timestamp format from gpg)
|
||||
CreatedAt string `json:"CreatedAt" example:"2023-01-15"`
|
||||
}
|
||||
|
||||
type gpgKeyListResponse struct {
|
||||
Keys []gpgKeyInfo `json:"Keys"`
|
||||
}
|
||||
|
||||
type gpgAddKeyParams struct {
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Add ASCII armored gpg public key, do not download from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
|
||||
// Keyserver to download keys provided in `GpgKeyID`
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// Keys do download from `Keyserver`, separated by space
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500 8B48AD6246925553"`
|
||||
}
|
||||
|
||||
type gpgDeleteKeyParams struct {
|
||||
// Keyring to delete keys from (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Key ID or fingerprint to delete
|
||||
GpgKeyID string `json:"GpgKeyID" example:"8B48AD6246925553"`
|
||||
}
|
||||
|
||||
// @Summary Add GPG Keys
|
||||
// @Description **Adds GPG keys to aptly keyring**
|
||||
// @Description
|
||||
// @Description Add GPG public keys for veryfing remote repositories for mirroring.
|
||||
// @Description
|
||||
// @Description Keys can be added in two ways:
|
||||
// @Description * By providing the ASCII armord key in `GpgKeyArmor` (leave Keyserver and GpgKeyID empty)
|
||||
// @Description * By providing a `Keyserver` and one or more key IDs in `GpgKeyID`, separated by space (leave GpgKeyArmor empty)
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgAddKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [post]
|
||||
func apiGPGAddKey(c *gin.Context) {
|
||||
b := gpgAddKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
b.Keyserver = utils.SanitizePath(b.Keyserver)
|
||||
b.GpgKeyID = utils.SanitizePath(b.GpgKeyID)
|
||||
b.GpgKeyArmor = utils.SanitizePath(b.GpgKeyArmor)
|
||||
// b.Keyring can be an absolute path
|
||||
|
||||
var err error
|
||||
args := []string{"--no-default-keyring", "--allow-non-selfsigned-uid"}
|
||||
keyring := "trustedkeys.gpg"
|
||||
if len(b.Keyring) > 0 {
|
||||
keyring = b.Keyring
|
||||
}
|
||||
args = append(args, "--keyring", keyring)
|
||||
if len(b.Keyserver) > 0 {
|
||||
args = append(args, "--keyserver", b.Keyserver)
|
||||
}
|
||||
if len(b.GpgKeyArmor) > 0 {
|
||||
var tempdir string
|
||||
tempdir, err = os.MkdirTemp(os.TempDir(), "aptly")
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(tempdir) }()
|
||||
|
||||
keypath := filepath.Join(tempdir, "key")
|
||||
keyfile, e := os.Create(keypath)
|
||||
if e != nil {
|
||||
AbortWithJSONError(c, 400, e)
|
||||
return
|
||||
}
|
||||
if _, e = keyfile.WriteString(b.GpgKeyArmor); e != nil {
|
||||
AbortWithJSONError(c, 400, e)
|
||||
}
|
||||
args = append(args, "--import", keypath)
|
||||
|
||||
}
|
||||
if len(b.GpgKeyID) > 0 {
|
||||
keys := strings.Fields(b.GpgKeyID)
|
||||
args = append(args, "--recv-keys")
|
||||
args = append(args, keys...)
|
||||
}
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
// it might happened that we have a situation with an erroneous
|
||||
// gpg command (e.g. when GpgKeyID and GpgKeyArmor is set).
|
||||
// there is no error handling for such as gpg will do this for us
|
||||
cmd := exec.Command(gpg, args...)
|
||||
fmt.Printf("running %s %s\n", gpg, strings.Join(args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
c.JSON(400, string(out))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// @Summary List GPG Keys
|
||||
// @Description **Lists all GPG keys in aptly keyring**
|
||||
// @Description
|
||||
// @Description Returns all public keys currently installed in the aptly GPG keyring.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Param keyring query string false "Keyring file to list keys from (default: trustedkeys.gpg)" example(trustedkeys.gpg)
|
||||
// @Produce json
|
||||
// @Success 200 {object} gpgKeyListResponse "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/keys [get]
|
||||
func apiGPGListKeys(c *gin.Context) {
|
||||
keyring := c.DefaultQuery("keyring", "trustedkeys.gpg")
|
||||
keyring = utils.SanitizePath(keyring)
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--with-colons",
|
||||
"--keyring", keyring,
|
||||
"--list-keys",
|
||||
}
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to list keys: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
keys := parseGPGOutput(string(out))
|
||||
c.JSON(200, gpgKeyListResponse{Keys: keys})
|
||||
}
|
||||
|
||||
// @Summary Delete GPG Key
|
||||
// @Description **Deletes a GPG key from aptly keyring**
|
||||
// @Description
|
||||
// @Description Removes a public key from the aptly GPG keyring. This is useful for removing
|
||||
// @Description compromised keys or cleaning up obsolete keys.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgDeleteKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [delete]
|
||||
func apiGPGDeleteKey(c *gin.Context) {
|
||||
b := gpgDeleteKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(b.GpgKeyID)) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("GpgKeyID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
b.GpgKeyID = utils.SanitizePath(b.GpgKeyID)
|
||||
// b.Keyring can be an absolute path
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--allow-non-selfsigned-uid",
|
||||
"--batch",
|
||||
"--yes",
|
||||
}
|
||||
|
||||
keyring := "trustedkeys.gpg"
|
||||
if len(b.Keyring) > 0 {
|
||||
keyring = b.Keyring
|
||||
}
|
||||
|
||||
args = append(args, "--keyring", keyring)
|
||||
args = append(args, "--delete-keys", b.GpgKeyID)
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
fmt.Printf("running %s %s\n", gpg, strings.Join(args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to delete key: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// parseGPGOutput parses the output of `gpg --with-colons --list-keys`
|
||||
// and returns a structured list of keys
|
||||
func parseGPGOutput(output string) []gpgKeyInfo {
|
||||
var keys []gpgKeyInfo
|
||||
var currentKey *gpgKeyInfo
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
recordType := parts[0]
|
||||
|
||||
// pub: public key record
|
||||
if recordType == "pub" {
|
||||
// Save previous key if it exists
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
// Create new key entry
|
||||
// Format: pub:trust:length:algo:keyid:created:expires:uidhash:...
|
||||
keyID := parts[4]
|
||||
if len(keyID) >= 16 {
|
||||
keyID = keyID[len(keyID)-16:] // Last 16 chars = short key ID
|
||||
}
|
||||
validity := parts[1]
|
||||
createdAt := parts[5]
|
||||
|
||||
currentKey = &gpgKeyInfo{
|
||||
KeyID: keyID,
|
||||
Validity: validity,
|
||||
CreatedAt: createdAt,
|
||||
UserIDs: []string{},
|
||||
Fingerprint: "",
|
||||
}
|
||||
}
|
||||
|
||||
// uid: user ID record
|
||||
if recordType == "uid" && currentKey != nil {
|
||||
// Format: uid:trust:created:expires:keyid:uidhash:uidtype:validity:userID:...
|
||||
if len(parts) >= 10 {
|
||||
userID := parts[9]
|
||||
if userID != "" {
|
||||
currentKey.UserIDs = append(currentKey.UserIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fpr: fingerprint record
|
||||
if recordType == "fpr" && currentKey != nil {
|
||||
// Format: fpr:::::::::fingerprint:
|
||||
if len(parts) >= 10 {
|
||||
fingerprint := parts[9]
|
||||
currentKey.Fingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last key
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GPGSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&GPGSuite{})
|
||||
|
||||
func (s *GPGSuite) withFakeGPG(c *C, scriptBody string, test func(scriptPath string)) {
|
||||
tempDir, err := os.MkdirTemp("", "aptly-fake-gpg")
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
scriptPath := filepath.Join(tempDir, "gpg")
|
||||
err = os.WriteFile(scriptPath, []byte(scriptBody), 0o755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
err = os.Setenv("PATH", tempDir+string(os.PathListSeparator)+oldPath)
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.Setenv("PATH", oldPath) }()
|
||||
|
||||
test(scriptPath)
|
||||
}
|
||||
|
||||
func (s *GPGSuite) fakeGPGScript(c *C, listOutput string, deleteOutput string, deleteError string) string {
|
||||
return "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then\n" +
|
||||
" echo 'gpg (GnuPG) 2.2.27'\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"args=\"$*\"\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--list-keys'; then\n" +
|
||||
" cat <<'EOF'\n" + listOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--delete-keys'; then\n" +
|
||||
" if [ -n \"" + strings.ReplaceAll(deleteError, "\n", "") + "\" ]; then\n" +
|
||||
" echo '" + strings.ReplaceAll(deleteError, "'", "'\\''") + "'\n" +
|
||||
" exit 1\n" +
|
||||
" fi\n" +
|
||||
" cat <<'EOF'\n" + deleteOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"echo 'unexpected invocation' >&2\n" +
|
||||
"exit 1\n"
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmpty tests parsing of empty GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputEmpty(c *C) {
|
||||
output := ""
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSingleKeyMinimal tests parsing a single key with minimal fields
|
||||
func (s *GPGSuite) TestParseGPGOutputSingleKeyMinimal(c *C) {
|
||||
// Minimal valid GPG output with one key
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.Validity, Equals, "u")
|
||||
c.Check(key.CreatedAt, Equals, "1611864000")
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
c.Check(key.UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleKeys tests parsing multiple keys
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleKeys(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:f:2048:1:A1B2C3D4E5F67890:1580592000:1612128000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1580592000::0987654321::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].Validity, Equals, "u")
|
||||
c.Check(keys[0].UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
|
||||
// Second key
|
||||
c.Check(keys[1].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(keys[1].Validity, Equals, "f")
|
||||
c.Check(keys[1].UserIDs, DeepEquals, []string{"Jane Smith <jane@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleUIDs tests a key with multiple user IDs
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john.doe@company.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.UserIDs, HasLen, 2)
|
||||
c.Check(key.UserIDs, DeepEquals, []string{
|
||||
"John Doe <john@example.com>",
|
||||
"John Doe <john.doe@company.com>",
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMalformedLines tests that malformed lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputMalformedLines(c *C) {
|
||||
// Mix of valid and invalid lines (too few fields)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
invalid:line:with:only:three:fields
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmptyLines tests that empty lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputEmptyLines(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputKeyWithoutUID tests a public key without user ID
|
||||
func (s *GPGSuite) TestParseGPGOutputKeyWithoutUID(c *C) {
|
||||
// Key without uid record (should still be included)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.UserIDs, HasLen, 0)
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputVariousValidity tests different validity values
|
||||
func (s *GPGSuite) TestParseGPGOutputVariousValidity(c *C) {
|
||||
output := `pub:u:4096:1:KEY1111111111111:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::Key1::::::::::0:
|
||||
fpr:::::::::1111111111111111111111111111111111111111:
|
||||
pub:f:4096:1:KEY2222222222222:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1611864000::1234567891::Key2::::::::::0:
|
||||
fpr:::::::::2222222222222222222222222222222222222222:
|
||||
pub:m:4096:1:KEY3333333333333:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:m::::1611864000::1234567892::Key3::::::::::0:
|
||||
fpr:::::::::3333333333333333333333333333333333333333:
|
||||
pub:n:4096:1:KEY4444444444444:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:n::::1611864000::1234567893::Key4::::::::::0:
|
||||
fpr:::::::::4444444444444444444444444444444444444444:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 4)
|
||||
|
||||
validities := []string{"u", "f", "m", "n"}
|
||||
for i, validity := range validities {
|
||||
c.Check(keys[i].Validity, Equals, validity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseGPGOutputShortKeyID tests that key IDs are shortened to 16 chars
|
||||
func (s *GPGSuite) TestParseGPGOutputShortKeyID(c *C) {
|
||||
// 40-character key ID that should be shortened to last 16 chars
|
||||
longKeyID := "0123456789ABCDEF0123456789ABCDEF8B48AD62"
|
||||
output := `pub:u:4096:1:` + longKeyID + `:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should extract the last 16 characters: 89ABCDEF8B48AD62
|
||||
c.Check(keys[0].KeyID, Equals, "89ABCDEF8B48AD62")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSpecialCharactersInUID tests user IDs with special characters
|
||||
func (s *GPGSuite) TestParseGPGOutputSpecialCharactersInUID(c *C) {
|
||||
// UID with Unicode characters and special formatting
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::J\xc3\xb6hn D\xc3\xb6\xc3\xa9 (D\xc3\xbcss) <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should preserve the encoded special characters
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysDefaultKeyring tests the HTTP endpoint with default keyring
|
||||
func (s *GPGSuite) TestAPIGPGListKeysDefaultKeyring(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:u:4096:1:8B48AD6246925553:1611864000:::::
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysWithKeyringParam tests the HTTP endpoint with custom keyring parameter
|
||||
func (s *GPGSuite) TestAPIGPGListKeysWithKeyringParam(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"cat <<'EOF'\n" +
|
||||
"pub:u:4096:1:8B48AD6246925553:1611864000:::::\n" +
|
||||
"fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:\n" +
|
||||
"EOF\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, reqErr := s.HTTPRequest("GET", "/api/gpg/keys?keyring=/custom.gpg", nil)
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
c.Check(string(argBytes), Matches, `(?s).*--keyring\ncustom\.gpg\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysResponseFormat tests that the response has the correct structure
|
||||
func (s *GPGSuite) TestAPIGPGListKeysResponseFormat(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:f:4096:1:A1B2C3D4E5F67890:1611864000:::::
|
||||
uid:f::::1611864000::1234567890::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(result.Keys[0].Validity, Equals, "f")
|
||||
c.Check(result.Keys[0].CreatedAt, Equals, "1611864000")
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEdgeCaseUIDWithoutFields tests UID record with missing fields
|
||||
func (s *GPGSuite) TestParseGPGOutputEdgeCaseUIDWithoutFields(c *C) {
|
||||
// UID record with fewer than 10 fields
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should not have user ID since it's in field 9 and this record is too short
|
||||
c.Check(keys[0].UserIDs, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputFingerprintWithoutCurrentKey tests FPR record appearing before any PUB
|
||||
func (s *GPGSuite) TestParseGPGOutputFingerprintWithoutCurrentKey(c *C) {
|
||||
// FPR record without a preceding PUB (should be ignored)
|
||||
output := `fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should only have one key with the correct fingerprint
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputComplexRealWorldExample tests real-world-like GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputComplexRealWorldExample(c *C) {
|
||||
// Real-world GPG output with multiple keys, UIDs, and other record types (sig, sub)
|
||||
// Note: sub and sig records are skipped as we only care about pub/uid/fpr
|
||||
realWorldOutput := `tru::1:1611864000:0:3:1:5
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:2023-01-15T00:00:00:::::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864100::1234567891::John Doe <john@work.com>::::::::::0:
|
||||
pub:f:2048:1:1234567890123456:1580592000:2022-12-31T00:00:00::u:::scESC:::::::23::0:
|
||||
fpr:::::::::F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9:
|
||||
uid:f::::1580592000::0987654321::Maintainer Key <maint@example.com>::::::::::0:`
|
||||
|
||||
keys := parseGPGOutput(realWorldOutput)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key should have 2 UIDs
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].UserIDs, HasLen, 2)
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
|
||||
// Second key should have 1 UID
|
||||
c.Check(keys[1].KeyID, Equals, "1234567890123456")
|
||||
c.Check(keys[1].UserIDs, HasLen, 1)
|
||||
c.Check(keys[1].Fingerprint, Equals, "F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputConsecutiveEmptyUIDs tests handling of consecutive empty user ID fields
|
||||
func (s *GPGSuite) TestParseGPGOutputConsecutiveEmptyUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should skip empty UID but include the non-empty one
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
c.Check(keys[0].UserIDs[0], Equals, "John Doe <john@example.com>")
|
||||
}
|
||||
|
||||
// TestGPGDeleteKeyParamsValidation tests gpgDeleteKeyParams validation
|
||||
func (s *GPGSuite) TestGPGDeleteKeyParamsValidation(c *C) {
|
||||
// This is a unit test that validates parameter structure (no HTTP needed)
|
||||
params := gpgDeleteKeyParams{
|
||||
Keyring: "custom.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
}
|
||||
c.Check(params.Keyring, Equals, "custom.gpg")
|
||||
c.Check(params.GpgKeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyMissingKeyID tests delete with missing key ID parameter
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyMissingKeyID(c *C) {
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
// GpgKeyID is missing
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyInvalidJSON tests delete with invalid JSON request
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyInvalidJSON(c *C) {
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader([]byte("invalid json")))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeySuccess tests successful key deletion
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeySuccess(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-delete-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--delete-keys'; then\n" +
|
||||
"echo 'deleted'\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
body, marshalErr := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "/trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(marshalErr, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, `"deleted\\n"`)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
argText := string(argBytes)
|
||||
c.Check(argText, Matches, `(?s).*--batch\n--yes\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--keyring\n/trustedkeys\.gpg\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--delete-keys\n8B48AD6246925553\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysCommandFailure tests list error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGListKeysCommandFailure(c *C) {
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"echo 'keyring missing'\n" +
|
||||
"exit 1\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to list keys: keyring missing.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyCommandFailure tests delete error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyCommandFailure(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, "", "", "delete failed"), func(_ string) {
|
||||
body, err := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to delete key: delete failed.*`)
|
||||
})
|
||||
}
|
||||
+26
-16
@@ -12,7 +12,27 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /api/graph.:ext?layout=[vertical|horizontal(default)]
|
||||
// @Summary Graph Output
|
||||
// @Description **Generate dependency graph**
|
||||
// @Description
|
||||
// @Description Command graph generates graph of dependencies:
|
||||
// @Description
|
||||
// @Description * between snapshots and mirrors (what mirror was used to create each snapshot)
|
||||
// @Description * between snapshots and local repos (what local repo was used to create snapshot)
|
||||
// @Description * between snapshots (pulling, merging, etc.)
|
||||
// @Description * between snapshots, local repos and published repositories (how snapshots were published).
|
||||
// @Description
|
||||
// @Description Graph is rendered to PNG file using graphviz package.
|
||||
// @Description
|
||||
// @Description Example URL: `http://localhost:8080/api/graph.svg?layout=vertical`
|
||||
// @Tags Status
|
||||
// @Produce image/png, image/svg+xml
|
||||
// @Param ext path string true "ext specifies desired file extension, e.g. .png, .svg."
|
||||
// @Param layout query string false "Change between a `horizontal` (default) and a `vertical` graph layout."
|
||||
// @Success 200 {object} []byte "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/graph.{ext} [get]
|
||||
func apiGraph(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
@@ -21,17 +41,7 @@ func apiGraph(c *gin.Context) {
|
||||
|
||||
ext := c.Params.ByName("ext")
|
||||
layout := c.Request.URL.Query().Get("layout")
|
||||
|
||||
factory := context.CollectionFactory()
|
||||
|
||||
factory.RemoteRepoCollection().Lock()
|
||||
defer factory.RemoteRepoCollection().Unlock()
|
||||
factory.LocalRepoCollection().Lock()
|
||||
defer factory.LocalRepoCollection().Unlock()
|
||||
factory.SnapshotCollection().Lock()
|
||||
defer factory.SnapshotCollection().Unlock()
|
||||
factory.PublishedRepoCollection().Lock()
|
||||
defer factory.PublishedRepoCollection().Unlock()
|
||||
factory := context.NewCollectionFactory()
|
||||
|
||||
graph, err := deb.BuildGraph(factory, layout)
|
||||
if err != nil {
|
||||
@@ -53,25 +63,25 @@ func apiGraph(c *gin.Context) {
|
||||
|
||||
stdin, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(stdin, buf)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = stdin.Close()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err = command.Output()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, fmt.Errorf("unable to execute dot: %s (is graphviz package installed?)", err))
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to execute dot: %s (is graphviz package installed?)", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
apiRequestsInFlightGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_api_http_requests_in_flight",
|
||||
Help: "Number of concurrent HTTP api requests currently handled.",
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
apiRequestsTotalCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "aptly_api_http_requests_total",
|
||||
Help: "Total number of api requests.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiRequestSizeSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_request_size_bytes",
|
||||
Help: "Api HTTP request size in bytes.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiResponseSizeSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_response_size_bytes",
|
||||
Help: "Api HTTP response size in bytes.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiRequestsDurationSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_request_duration_seconds",
|
||||
Help: "Duration of api requests in seconds.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiVersionGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_build_info",
|
||||
Help: "Metric with a constant '1' value labeled by version and goversion from which aptly was built.",
|
||||
},
|
||||
[]string{"version", "goversion"},
|
||||
)
|
||||
apiFilesUploadedCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "aptly_api_files_uploaded_total",
|
||||
Help: "Total number of uploaded files labeled by upload directory.",
|
||||
},
|
||||
[]string{"directory"},
|
||||
)
|
||||
apiReposPackageCountGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_repos_package_count",
|
||||
Help: "Current number of published packages labeled by source, distribution and component.",
|
||||
},
|
||||
[]string{"source", "distribution", "component"},
|
||||
)
|
||||
)
|
||||
|
||||
type metricsCollectorRegistrar struct {
|
||||
hasRegistered bool
|
||||
}
|
||||
|
||||
func (r *metricsCollectorRegistrar) Register(router *gin.Engine) {
|
||||
if !r.hasRegistered {
|
||||
apiVersionGauge.WithLabelValues(aptly.Version, runtime.Version()).Set(1)
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
r.hasRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
var MetricsCollectorRegistrar = metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
func countPackagesByRepos() {
|
||||
err := context.NewCollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
err := context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf(
|
||||
"Error %s found while determining package count for metrics endpoint (prefix:%s / distribution:%s / component:%s\n).",
|
||||
err, repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
log.Warn().Msg(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
components := repo.Components()
|
||||
for _, c := range components {
|
||||
count := float64(len(repo.RefList(c).Refs))
|
||||
apiReposPackageCountGauge.WithLabelValues(fmt.Sprintf("%s", (repo.SourceNames())), repo.Distribution, c).Set(count)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error %s found while listing published repos for metrics endpoint", err)
|
||||
log.Warn().Msg(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Only use base path as label value (e.g.: /api/repos) because of time series cardinality
|
||||
// See https://prometheus.io/docs/practices/naming/#labels
|
||||
func getBasePath(c *gin.Context) string {
|
||||
segment0, err := getURLSegment(c.Request.URL.Path, 0)
|
||||
if err != nil {
|
||||
return "/"
|
||||
}
|
||||
segment1, err := getURLSegment(c.Request.URL.Path, 1)
|
||||
if err != nil {
|
||||
return *segment0
|
||||
}
|
||||
|
||||
return *segment0 + *segment1
|
||||
}
|
||||
|
||||
func getURLSegment(url string, idx int) (*string, error) {
|
||||
urlSegments := strings.Split(url, "/")
|
||||
// Remove segment at index 0 because it's an empty string
|
||||
urlSegments = urlSegments[1:cap(urlSegments)]
|
||||
|
||||
if len(urlSegments) <= idx {
|
||||
return nil, fmt.Errorf("index %d out of range, only has %d url segments", idx, len(urlSegments))
|
||||
}
|
||||
|
||||
segmentAtIndex := urlSegments[idx]
|
||||
s := fmt.Sprintf("/%s", segmentAtIndex)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func instrumentHandlerInFlight(g *prometheus.GaugeVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
g.WithLabelValues(c.Request.Method, pathFunc(c)).Inc()
|
||||
defer g.WithLabelValues(c.Request.Method, pathFunc(c)).Dec()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerCounter(counter *prometheus.CounterVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
counter.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerRequestSize(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(float64(c.Request.ContentLength))
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerResponseSize(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
var responseSize = math.Max(float64(c.Writer.Size()), 0)
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(responseSize)
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerDuration(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
now := time.Now()
|
||||
c.Next()
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(time.Since(now).Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
// JSONLogger is a gin middleware that takes an instance of Logger and uses it for writing access
|
||||
// logs that include error messages if there are any.
|
||||
func JSONLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
ts := time.Now()
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
errorMessage := strings.TrimSuffix(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
|
||||
l := log.With().Str("remote", c.ClientIP()).Logger().
|
||||
With().Str("method", c.Request.Method).Logger().
|
||||
With().Str("path", path).Logger().
|
||||
With().Str("protocol", c.Request.Proto).Logger().
|
||||
With().Str("code", fmt.Sprint(c.Writer.Status())).Logger().
|
||||
With().Str("latency", ts.Sub(start).String()).Logger().
|
||||
With().Str("agent", c.Request.UserAgent()).Logger()
|
||||
|
||||
if c.Writer.Status() >= 400 && c.Writer.Status() < 500 {
|
||||
l.Warn().Msg(errorMessage)
|
||||
} else if c.Writer.Status() >= 500 {
|
||||
l.Error().Msg(errorMessage)
|
||||
} else {
|
||||
l.Info().Msg(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MiddlewareSuite struct {
|
||||
router http.Handler
|
||||
context *gin.Context
|
||||
logReader *os.File
|
||||
logWriter *os.File
|
||||
}
|
||||
|
||||
var _ = Suite(&MiddlewareSuite{})
|
||||
|
||||
func (s *MiddlewareSuite) SetUpTest(c *C) {
|
||||
r, w, err := os.Pipe()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
utils.SetupJSONLogger("debug", w)
|
||||
mw := JSONLogger()
|
||||
|
||||
router := gin.New()
|
||||
router.UseRawPath = true
|
||||
router.Use(mw)
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
root := router.Group("/api")
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
root.GET("/ready", apiReady(isReady))
|
||||
root.GET("/healthy", apiHealthy)
|
||||
|
||||
s.router = router
|
||||
s.logReader = r
|
||||
s.logWriter = w
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TearDownTest(c *C) {
|
||||
s.router = nil
|
||||
s.context = nil
|
||||
s.logReader = nil
|
||||
s.logWriter = nil
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) HTTPRequest(method string, url string, body io.Reader) {
|
||||
recorder := httptest.NewRecorder()
|
||||
s.context, _ = gin.CreateTestContext(recorder)
|
||||
req, _ := http.NewRequestWithContext(s.context, method, url, body)
|
||||
s.context.Request = req
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(httptest.NewRecorder(), req)
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware4xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "warn")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["method"]; ok {
|
||||
c.Check(val, Equals, "GET")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'method' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["path"]; ok {
|
||||
c.Check(val, Equals, "/")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'path' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["protocol"]; ok {
|
||||
c.Check(val, Equals, "HTTP/1.1")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'protocol' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["code"]; ok {
|
||||
c.Check(val, Equals, "404")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'code' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["remote"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'remote' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["latency"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'latency' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["agent"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'agent' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["time"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'time' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware2xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware5xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/ready", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "error")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddlewareRaw(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy?test=raw", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
fmt.Println(capturedOutput)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestGetBasePath(c *C) {
|
||||
s.HTTPRequest(http.MethodGet, "", nil)
|
||||
path := getBasePath(s.context)
|
||||
c.Check(path, Equals, "/")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/api")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/repos/testRepo", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/api/repos")
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestGetURLSegment(c *C) {
|
||||
url := "/"
|
||||
segment, err := getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/")
|
||||
|
||||
_, err = getURLSegment(url, 1)
|
||||
if err == nil {
|
||||
c.Error("Invalid return value")
|
||||
}
|
||||
|
||||
url = "/api"
|
||||
segment, err = getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
_, err = getURLSegment(url, 1)
|
||||
if err == nil {
|
||||
c.Error("Invalid return value")
|
||||
}
|
||||
|
||||
url = "/api/repos/testRepo"
|
||||
segment, err = getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
segment, err = getURLSegment(url, 1)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/repos")
|
||||
}
|
||||
+792
@@ -0,0 +1,792 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func getVerifier(keyRings []string) (pgp.Verifier, error) {
|
||||
verifier := context.GetVerifier()
|
||||
for _, keyRing := range keyRings {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
// stringSlicesEqual compares two string slices for equality (order matters)
|
||||
func stringSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// uniqueStrings returns a new slice with only unique strings from the input, sorted
|
||||
func uniqueStrings(input []string) []string {
|
||||
if len(input) == 0 {
|
||||
return input
|
||||
}
|
||||
seen := make(map[string]struct{}, len(input))
|
||||
result := make([]string, 0, len(input))
|
||||
for _, s := range input {
|
||||
if _, ok := seen[s]; !ok {
|
||||
seen[s] = struct{}{}
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// @Summary List Mirrors
|
||||
// @Description **Show list of currently available mirrors**
|
||||
// @Description Each mirror is returned as in “show” API.
|
||||
// @Tags Mirrors
|
||||
// @Produce json
|
||||
// @Success 200 {array} remoteRepoResponse
|
||||
// @Router /api/mirrors [get]
|
||||
func apiMirrorsList(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
result := []remoteRepoResponse{}
|
||||
err := collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newRemoteRepoResponse(repo))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
type mirrorCreateParams struct {
|
||||
// Name of mirror to be created
|
||||
Name string `binding:"required" json:"Name" example:"mirror2"`
|
||||
// Url of the archive to mirror
|
||||
ArchiveURL string `binding:"required" json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Distribution name to mirror
|
||||
Distribution string ` json:"Distribution" example:"'buster', for flat repositories use './'"`
|
||||
// Package query that is applied to mirror packages
|
||||
Filter string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Components to mirror, if not specified aptly would fetch all components
|
||||
Components []string ` json:"Components" example:"main"`
|
||||
// Limit mirror to those architectures, if not specified aptly would fetch all architectures
|
||||
Architectures []string ` json:"Architectures" example:"amd64"`
|
||||
// Gpg keyring(s) for verifying Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller bool ` json:"DownloadInstaller"`
|
||||
// Set "true" to mirror AppStream (DEP-11) metadata
|
||||
DownloadAppStream bool ` json:"DownloadAppStream"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to skip if the given components are in the Release file
|
||||
SkipComponentCheck bool ` json:"SkipComponentCheck"`
|
||||
// Set "true" to skip the verification of architectures
|
||||
SkipArchitectureCheck bool ` json:"SkipArchitectureCheck"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures bool ` json:"IgnoreSignatures"`
|
||||
}
|
||||
|
||||
// @Summary Create Mirror
|
||||
// @Description **Create a mirror of a remote repository**
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body mirrorCreateParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/mirrors [post]
|
||||
func apiMirrorsCreate(c *gin.Context) {
|
||||
var err error
|
||||
var b mirrorCreateParams
|
||||
|
||||
b.DownloadSources = context.Config().DownloadSourcePackages
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
b.Architectures = context.ArchitecturesList()
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
if strings.HasPrefix(b.ArchiveURL, "ppa:") {
|
||||
b.ArchiveURL, b.Distribution, b.Components, err = deb.ParsePPA(b.ArchiveURL, context.Config())
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.Filter != "" {
|
||||
_, err = query.Parse(b.Filter)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(b.Name, b.ArchiveURL, b.Distribution, b.Components, b.Architectures,
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller, b.DownloadAppStream)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
repo.Filter = b.Filter
|
||||
repo.FilterWithDeps = b.FilterWithDeps
|
||||
repo.SkipComponentCheck = b.SkipComponentCheck
|
||||
repo.SkipArchitectureCheck = b.SkipArchitectureCheck
|
||||
repo.DownloadSources = b.DownloadSources
|
||||
repo.DownloadUdebs = b.DownloadUdebs
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
downloader := context.NewDownloader(nil)
|
||||
err = repo.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to fetch mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.Add(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to add mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(201, repo)
|
||||
}
|
||||
|
||||
// @Summary Delete Mirror
|
||||
// @Description **Delete a mirror**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Param force query int true "force: 1 to enable"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 403 {object} Error "Unable to delete mirror with snapshots"
|
||||
// @Failure 500 {object} Error "Unable to delete"
|
||||
// @Router /api/mirrors/{name} [delete]
|
||||
func apiMirrorsDrop(c *gin.Context) {
|
||||
name := c.Params.ByName("name")
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
mirrorCollection := collectionFactory.RemoteRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
repo, err := mirrorCollection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to drop: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete mirror %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := repo.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.ByRemoteRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusForbidden, Value: nil}, fmt.Errorf("won't delete mirror with snapshots, use 'force=1' to override")
|
||||
}
|
||||
}
|
||||
|
||||
err = mirrorCollection.Drop(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusNoContent, Value: nil}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get Mirror Info
|
||||
// @Description **Get mirror information by name**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [get]
|
||||
func apiMirrorsShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
// @Summary List Mirror Packages
|
||||
// @Description **Get a list of packages from a mirror**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Param q query string false "search query"
|
||||
// @Param format query string false "format: `details` for more detailed information"
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.Package "List of Packages"
|
||||
// @Failure 400 {object} Error "Unable to determine list of architectures"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name}/packages [get]
|
||||
func apiMirrorsPackages(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
}
|
||||
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show package list, mirror hasn't been downloaded yet"))
|
||||
return
|
||||
}
|
||||
|
||||
reflist := repo.RefList()
|
||||
result := []*deb.Package{}
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
queryS := c.Request.URL.Query().Get("q")
|
||||
if queryS != "" {
|
||||
q, err := query.Parse(c.Request.URL.Query().Get("q"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
withDeps := c.Request.URL.Query().Get("withDeps") == "1"
|
||||
architecturesList := []string{}
|
||||
|
||||
if withDeps {
|
||||
if len(context.ArchitecturesList()) > 0 {
|
||||
architecturesList = context.ArchitecturesList()
|
||||
} else {
|
||||
architecturesList = list.Architectures(false)
|
||||
}
|
||||
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to determine list of architectures, please specify explicitly"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
|
||||
list, err = list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{q},
|
||||
WithDependencies: withDeps,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to search: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
} else {
|
||||
c.JSON(200, list.Strings())
|
||||
}
|
||||
}
|
||||
|
||||
type mirrorEditParams struct {
|
||||
// Package query that is applied to mirror packages
|
||||
Filter *string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps *bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller *bool `json:"DownloadInstaller"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources *bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs *bool ` json:"DownloadUdebs"`
|
||||
// URL of the archive to mirror
|
||||
ArchiveURL *string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Comma separated list of architectures
|
||||
Architectures *[]string `json:"Architectures" example:"amd64"`
|
||||
// Gpg keyring(s) for verifying Release file if a mirror update is required.
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures *bool ` json:"IgnoreSignatures"`
|
||||
}
|
||||
|
||||
// @Summary Edit Mirror
|
||||
// @Description **Edit mirror config**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name to edit"
|
||||
// @Consume json
|
||||
// @Param request body mirrorEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo "Mirror was edited successfully"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 409 {object} Error "Aptly db locked"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [post]
|
||||
func apiMirrorsEdit(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
b mirrorEditParams
|
||||
repo *deb.RemoteRepo
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fetchMirror := false
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
|
||||
if b.Filter != nil {
|
||||
repo.Filter = *b.Filter
|
||||
}
|
||||
if b.FilterWithDeps != nil {
|
||||
repo.FilterWithDeps = *b.FilterWithDeps
|
||||
}
|
||||
if b.DownloadInstaller != nil {
|
||||
repo.DownloadInstaller = *b.DownloadInstaller
|
||||
}
|
||||
if b.DownloadSources != nil {
|
||||
repo.DownloadSources = *b.DownloadSources
|
||||
}
|
||||
if b.DownloadUdebs != nil {
|
||||
repo.DownloadUdebs = *b.DownloadUdebs
|
||||
}
|
||||
if b.ArchiveURL != nil && *b.ArchiveURL != repo.ArchiveRoot {
|
||||
repo.SetArchiveRoot(*b.ArchiveURL)
|
||||
fetchMirror = true
|
||||
}
|
||||
if b.Architectures != nil {
|
||||
uniqueArchitectures := uniqueStrings(*b.Architectures)
|
||||
if !stringSlicesEqual(uniqueArchitectures, uniqueStrings(repo.Architectures)) {
|
||||
repo.Architectures = uniqueArchitectures
|
||||
fetchMirror = true
|
||||
}
|
||||
}
|
||||
if b.IgnoreSignatures != nil {
|
||||
ignoreSignatures = *b.IgnoreSignatures
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to edit: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
|
||||
if fetchMirror {
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
type mirrorUpdateParams struct {
|
||||
// Change mirror name to `Name`
|
||||
Name string ` json:"Name" example:"mirror1"`
|
||||
// Gpg keyring(s) for verifing Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to ignore checksum errors
|
||||
IgnoreChecksums bool ` json:"IgnoreChecksums"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures bool ` json:"IgnoreSignatures"`
|
||||
// Set "true" to force a mirror update even if another process is already updating the mirror (use with caution!)
|
||||
ForceUpdate bool ` json:"ForceUpdate"`
|
||||
// Set "true" to skip downloading already downloaded packages
|
||||
SkipExistingPackages bool ` json:"SkipExistingPackages"`
|
||||
// Set "true" to download only the latest version per package/architecture
|
||||
LatestOnly bool ` json:"LatestOnly"`
|
||||
}
|
||||
|
||||
// @Summary Update Mirror
|
||||
// @Description **Update Mirror and download packages**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name to update"
|
||||
// @Consume json
|
||||
// @Param request body mirrorUpdateParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "Mirror was updated successfully"
|
||||
// @Success 202 {object} task.Task "Mirror is being updated"
|
||||
// @Failure 400 {object} Error "Unable to determine list of architectures"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [put]
|
||||
func apiMirrorsUpdate(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
remote *deb.RemoteRepo
|
||||
b mirrorUpdateParams
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
remote, err = collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
b.Name = remote.Name
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
|
||||
log.Info().Msgf("%s: Starting mirror update", b.Name)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != remote.Name {
|
||||
_, err = collection.ByName(b.Name)
|
||||
if err == nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to rename: mirror %s already exists", b.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(remote.Key())}
|
||||
maybeRunTaskInBackground(c, "Update mirror "+b.Name, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
|
||||
downloader := context.NewDownloader(out)
|
||||
err := remote.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if !b.ForceUpdate {
|
||||
err = remote.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, remote.SkipComponentCheck)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if remote.DownloadAppStream && !remote.IsFlat() {
|
||||
err = remote.DownloadAppStreamFiles(out, downloader,
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection(nil), b.IgnoreChecksums)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if remote.Filter != "" {
|
||||
var filterQuery deb.PackageQuery
|
||||
|
||||
filterQuery, err = query.Parse(remote.Filter)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
_, _, err = remote.ApplyFilter(context.DependencyOptions(), filterQuery, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages, b.LatestOnly)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// on any interruption, unlock the mirror
|
||||
e := context.ReOpenDatabase()
|
||||
if e == nil {
|
||||
remote.MarkAsIdle()
|
||||
_ = collection.Update(remote)
|
||||
}
|
||||
}()
|
||||
|
||||
remote.MarkAsUpdating()
|
||||
err = collection.Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.GoContextHandleSignals()
|
||||
|
||||
count := len(queue)
|
||||
taskDetail := struct {
|
||||
TotalDownloadSize int64
|
||||
RemainingDownloadSize int64
|
||||
TotalNumberOfPackages int
|
||||
RemainingNumberOfPackages int
|
||||
}{
|
||||
downloadSize, downloadSize, count, count,
|
||||
}
|
||||
detail.Store(taskDetail)
|
||||
|
||||
downloadQueue := make(chan int)
|
||||
taskFinished := make(chan *deb.PackageDownloadTask)
|
||||
|
||||
var (
|
||||
errors []string
|
||||
errLock sync.Mutex
|
||||
)
|
||||
|
||||
pushError := func(err error) {
|
||||
errLock.Lock()
|
||||
errors = append(errors, err.Error())
|
||||
errLock.Unlock()
|
||||
}
|
||||
|
||||
go func() {
|
||||
for idx := range queue {
|
||||
select {
|
||||
case downloadQueue <- idx:
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
close(downloadQueue)
|
||||
}()
|
||||
|
||||
// update of task details need to be done in order
|
||||
go func() {
|
||||
for {
|
||||
task, ok := <-taskFinished
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
taskDetail.RemainingDownloadSize -= task.File.Checksums.Size
|
||||
taskDetail.RemainingNumberOfPackages--
|
||||
detail.Store(taskDetail)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msgf("%s: Spawning background processes...", b.Name)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < context.Config().DownloadConcurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case idx, ok := <-downloadQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
task := &queue[idx]
|
||||
|
||||
var e error
|
||||
|
||||
// provision download location
|
||||
if pp, ok := context.PackagePool().(aptly.LocalPackagePool); ok {
|
||||
task.TempDownPath, e = pp.GenerateTempPath(task.File.Filename)
|
||||
} else {
|
||||
var file *os.File
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
// download file...
|
||||
e = context.Downloader().DownloadWithChecksum(
|
||||
context,
|
||||
remote.PackageURL(task.File.DownloadURL()).String(),
|
||||
task.TempDownPath,
|
||||
&task.File.Checksums,
|
||||
b.IgnoreChecksums)
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
// and import it back to the pool
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, collectionFactory.ChecksumCollection(nil))
|
||||
if err != nil {
|
||||
//return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to import file: %s", err)
|
||||
pushError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// update "attached" files if any
|
||||
for _, additionalAtask := range task.Additional {
|
||||
additionalAtask.File.PoolPath = task.File.PoolPath
|
||||
additionalAtask.File.Checksums = task.File.Checksums
|
||||
}
|
||||
|
||||
task.Done = true
|
||||
taskFinished <- task
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all download goroutines to finish
|
||||
log.Info().Msgf("%s: Waiting for background processes to finish...", b.Name)
|
||||
wg.Wait()
|
||||
log.Info().Msgf("%s: Background processes finished", b.Name)
|
||||
close(taskFinished)
|
||||
|
||||
defer func() {
|
||||
for _, task := range queue {
|
||||
if task.TempDownPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(task.TempDownPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Failed to delete %s: %v\n", task.TempDownPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-context.Done():
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: interrupted")
|
||||
default:
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
log.Info().Msgf("%s: Unable to update because of previous errors", b.Name)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Finalizing download...", b.Name)
|
||||
_ = remote.FinalizeDownload(collectionFactory, out)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Mirror updated successfully", b.Name)
|
||||
return &task.ProcessReturnValue{Code: http.StatusNoContent, Value: nil}, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MirrorSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MirrorSuite{})
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrors(c *C) {
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
response, _ := s.HTTPRequest("DELETE", "/api/mirrors/does-not-exist", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
c.Check(response.Body.String(), Equals, "{\"error\":\"unable to drop: mirror with name does-not-exist not found\"}")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirrorFlatWithAppStream(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "test-flat-appstream",
|
||||
"ArchiveURL": "http://example.com/repo/",
|
||||
"Distribution": "./",
|
||||
"DownloadAppStream": true,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/mirrors", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, ".*AppStream.*flat.*")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.ExpectFailure("Need to mock downloads")
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
"ArchiveURL": "foobar",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
response, err := s.HTTPRequest("POST", "/api/mirrors", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Equals, "")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = collection.Add(repo)
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), makePackageRefList(c).Encode())
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, mirror := range mirrors {
|
||||
if mirror["Name"] == "count-mirror" {
|
||||
found = true
|
||||
value, ok := mirror["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*unable to show:.*")
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import "github.com/aptly-dev/aptly/deb"
|
||||
|
||||
type remoteRepoResponse struct {
|
||||
*deb.RemoteRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type localRepoResponse struct {
|
||||
*deb.LocalRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type snapshotResponse struct {
|
||||
*deb.Snapshot
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
func newRemoteRepoResponse(repo *deb.RemoteRepo) remoteRepoResponse {
|
||||
return remoteRepoResponse{RemoteRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newLocalRepoResponse(repo *deb.LocalRepo) localRepoResponse {
|
||||
return localRepoResponse{LocalRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newSnapshotResponse(snapshot *deb.Snapshot) snapshotResponse {
|
||||
return snapshotResponse{Snapshot: snapshot, NumPackages: snapshot.NumPackages()}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func makePackageRefList(c *C) *deb.PackageRefList {
|
||||
list := deb.NewPackageList()
|
||||
c.Assert(list.Add(&deb.Package{Name: "libcount", Version: "1.0", Architecture: "amd64"}), IsNil)
|
||||
c.Assert(list.Add(&deb.Package{Name: "appcount", Version: "2.0", Architecture: "all"}), IsNil)
|
||||
return deb.NewPackageRefListFromPackageList(list)
|
||||
}
|
||||
|
||||
func putRawDBValue(c *C, s *APISuite, key []byte, value []byte) {
|
||||
db, err := s.context.Database()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(db.Put(key, value), IsNil)
|
||||
}
|
||||
+28
-3
@@ -1,16 +1,41 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "github.com/aptly-dev/aptly/deb" // for swagger
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /api/packages/:key
|
||||
// @Summary Get Package Info
|
||||
// @Description **Show information about package by package key**
|
||||
// @Description Package keys could be obtained from various GET .../packages APIs.
|
||||
// @Tags Packages
|
||||
// @Produce json
|
||||
// @Param key path string true "package key (unique package identifier)"
|
||||
// @Success 200 {object} deb.Package "OK"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/packages/{key} [get]
|
||||
func apiPackagesShow(c *gin.Context) {
|
||||
p, err := context.CollectionFactory().PackageCollection().ByKey([]byte(c.Params.ByName("key")))
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
p, err := collectionFactory.PackageCollection().ByKey([]byte(c.Params.ByName("key")))
|
||||
if err != nil {
|
||||
c.AbortWithError(404, err)
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, p)
|
||||
}
|
||||
|
||||
// @Summary List Packages
|
||||
// @Description **Get list of packages**
|
||||
// @Tags Packages
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Param q query string false "search query"
|
||||
// @Param format query string false "format: `details` for more detailed information"
|
||||
// @Success 200 {array} string "List of packages"
|
||||
// @Router /api/packages [get]
|
||||
func apiPackages(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PackageCollection()
|
||||
showPackages(c, collection.AllPackageRefs(), collectionFactory)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PackagesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagesSuite{})
|
||||
|
||||
func (s *PackagesSuite) TestPackagesGetMaximumVersion(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/repos/dummy/packages?maximumVersion=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
}
|
||||
+961
-211
File diff suppressed because it is too large
Load Diff
+740
-188
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ReposSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ReposSuite{})
|
||||
|
||||
func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo := deb.NewLocalRepo("count-repo-list", "")
|
||||
repo.UpdateRefList(makePackageRefList(c))
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var repos []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &repos)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, repo := range repos {
|
||||
if repo["Name"] == "count-repo-list" {
|
||||
found = true
|
||||
value, ok := repo["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *ReposSuite) TestGetReposReturns500OnCorruptRefList(c *C) {
|
||||
body, err := json.Marshal(gin.H{"Name": "broken-repo-list"})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 201)
|
||||
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo, err := collection.ByName("broken-repo-list")
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err = s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
+177
-51
@@ -2,114 +2,240 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// _ "github.com/aptly-dev/aptly/docs" // import docs
|
||||
// swaggerFiles "github.com/swaggo/files"
|
||||
// ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
var context *ctx.AptlyContext
|
||||
|
||||
// @Summary Get Metrics
|
||||
// @Description **Get Prometheus Metrics**
|
||||
// @Tags Status
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string Metrics
|
||||
// @Router /api/metrics [get]
|
||||
func apiMetricsGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
countPackagesByRepos()
|
||||
promhttp.Handler().ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func redirectSwagger(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/docs/index.html" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
if c.Request.URL.Path == "/docs/" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
if c.Request.URL.Path == "/docs" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// Router returns prebuilt with routes http.Handler
|
||||
func Router(c *ctx.AptlyContext) http.Handler {
|
||||
if aptly.EnableDebug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
context = c
|
||||
|
||||
router := gin.Default()
|
||||
router.Use(gin.ErrorLogger())
|
||||
router.UseRawPath = true
|
||||
|
||||
if c.Config().LogFormat == "json" {
|
||||
gin.DefaultWriter = utils.LogWriter{Logger: log.Logger}
|
||||
router.Use(JSONLogger())
|
||||
} else {
|
||||
router.Use(gin.Logger())
|
||||
}
|
||||
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
// if c.Config().EnableSwaggerEndpoint {
|
||||
// router.GET("docs.html", func(c *gin.Context) {
|
||||
// c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
// })
|
||||
// router.Use(redirectSwagger)
|
||||
// url := ginSwagger.URL("/docs/doc.json")
|
||||
// router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
// }
|
||||
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
MetricsCollectorRegistrar.Register(router)
|
||||
}
|
||||
|
||||
if c.Config().ServeInAPIMode {
|
||||
router.GET("/repos/", reposListInAPIMode(c.Config().FileSystemPublishRoots))
|
||||
router.GET("/repos/:storage/*pkgPath", reposServeInAPIMode)
|
||||
}
|
||||
|
||||
api := router.Group("/api")
|
||||
if context.Flags().Lookup("no-lock").Value.Get().(bool) {
|
||||
// We use a goroutine to count the number of
|
||||
// concurrent requests. When no more requests are
|
||||
// running, we close the database to free the lock.
|
||||
requests := make(chan dbRequest)
|
||||
dbRequests = make(chan dbRequest)
|
||||
|
||||
go acquireDatabase(requests)
|
||||
go acquireDatabase()
|
||||
|
||||
router.Use(func(c *gin.Context) {
|
||||
api.Use(func(c *gin.Context) {
|
||||
var err error
|
||||
|
||||
errCh := make(chan error)
|
||||
requests <- dbRequest{acquiredb, errCh}
|
||||
dbRequests <- dbRequest{acquiredb, errCh}
|
||||
|
||||
err = <-errCh
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
requests <- dbRequest{releasedb, errCh}
|
||||
dbRequests <- dbRequest{releasedb, errCh}
|
||||
err = <-errCh
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
AbortWithJSONError(c, 500, err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
})
|
||||
|
||||
} else {
|
||||
go cacheFlusher()
|
||||
}
|
||||
|
||||
root := router.Group("/api")
|
||||
|
||||
{
|
||||
root.GET("/version", apiVersion)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/repos", apiReposList)
|
||||
root.POST("/repos", apiReposCreate)
|
||||
root.GET("/repos/:name", apiReposShow)
|
||||
root.PUT("/repos/:name", apiReposEdit)
|
||||
root.DELETE("/repos/:name", apiReposDrop)
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
api.GET("/metrics", apiMetricsGet())
|
||||
}
|
||||
api.GET("/version", apiVersion)
|
||||
api.GET("/storage", apiDiskFree)
|
||||
|
||||
root.GET("/repos/:name/packages", apiReposPackagesShow)
|
||||
root.POST("/repos/:name/packages", apiReposPackagesAdd)
|
||||
root.DELETE("/repos/:name/packages", apiReposPackagesDelete)
|
||||
|
||||
root.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
|
||||
root.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
|
||||
|
||||
root.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
defer isReady.Store(true)
|
||||
api.GET("/ready", apiReady(isReady))
|
||||
api.GET("/healthy", apiHealthy)
|
||||
}
|
||||
|
||||
{
|
||||
root.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
|
||||
api.GET("/repos", apiReposList)
|
||||
api.POST("/repos", apiReposCreate)
|
||||
api.GET("/repos/:name", apiReposShow)
|
||||
api.PUT("/repos/:name", apiReposEdit)
|
||||
api.DELETE("/repos/:name", apiReposDrop)
|
||||
|
||||
api.GET("/repos/:name/packages", apiReposPackagesShow)
|
||||
api.POST("/repos/:name/packages", apiReposPackagesAdd)
|
||||
api.DELETE("/repos/:name/packages", apiReposPackagesDelete)
|
||||
|
||||
api.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
|
||||
api.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
|
||||
api.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
|
||||
|
||||
api.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
|
||||
api.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
|
||||
|
||||
api.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/files", apiFilesListDirs)
|
||||
root.POST("/files/:dir", apiFilesUpload)
|
||||
root.GET("/files/:dir", apiFilesListFiles)
|
||||
root.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
root.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
api.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/publish", apiPublishList)
|
||||
root.POST("/publish", apiPublishRepoOrSnapshot)
|
||||
root.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
|
||||
root.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||
root.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||
api.GET("/mirrors", apiMirrorsList)
|
||||
api.GET("/mirrors/:name", apiMirrorsShow)
|
||||
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
api.POST("/mirrors", apiMirrorsCreate)
|
||||
api.POST("/mirrors/:name", apiMirrorsEdit)
|
||||
api.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
api.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/snapshots", apiSnapshotsList)
|
||||
root.POST("/snapshots", apiSnapshotsCreate)
|
||||
root.PUT("/snapshots/:name", apiSnapshotsUpdate)
|
||||
root.GET("/snapshots/:name", apiSnapshotsShow)
|
||||
root.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
|
||||
root.DELETE("/snapshots/:name", apiSnapshotsDrop)
|
||||
root.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
|
||||
api.GET("/gpg/keys", apiGPGListKeys)
|
||||
api.POST("/gpg/key", apiGPGAddKey)
|
||||
api.DELETE("/gpg/key", apiGPGDeleteKey)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/packages/:key", apiPackagesShow)
|
||||
api.GET("/s3", apiS3List)
|
||||
}
|
||||
|
||||
{
|
||||
root.GET("/graph.:ext", apiGraph)
|
||||
api.GET("/files", apiFilesListDirs)
|
||||
api.POST("/files/:dir", apiFilesUpload)
|
||||
api.GET("/files/:dir", apiFilesListFiles)
|
||||
api.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
api.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/publish", apiPublishList)
|
||||
api.GET("/publish/:prefix/:distribution", apiPublishShow)
|
||||
api.POST("/publish", apiPublishRepoOrSnapshot)
|
||||
api.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
|
||||
api.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||
api.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||
api.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
|
||||
api.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
|
||||
api.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/snapshots", apiSnapshotsList)
|
||||
api.POST("/snapshots", apiSnapshotsCreate)
|
||||
api.PUT("/snapshots/:name", apiSnapshotsUpdate)
|
||||
api.GET("/snapshots/:name", apiSnapshotsShow)
|
||||
api.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
|
||||
api.DELETE("/snapshots/:name", apiSnapshotsDrop)
|
||||
api.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
|
||||
api.POST("/snapshots/:name/merge", apiSnapshotsMerge)
|
||||
api.POST("/snapshots/:name/pull", apiSnapshotsPull)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/packages/:key", apiPackagesShow)
|
||||
api.GET("/packages", apiPackages)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/graph.:ext", apiGraph)
|
||||
}
|
||||
{
|
||||
api.POST("/db/cleanup", apiDBCleanup)
|
||||
}
|
||||
{
|
||||
api.GET("/tasks", apiTasksList)
|
||||
api.POST("/tasks-clear", apiTasksClear)
|
||||
api.GET("/tasks-wait", apiTasksWait)
|
||||
api.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
|
||||
api.GET("/tasks/:id/output", apiTasksOutputShow)
|
||||
api.GET("/tasks/:id/detail", apiTasksDetailShow)
|
||||
api.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
|
||||
api.GET("/tasks/:id", apiTasksShow)
|
||||
api.DELETE("/tasks/:id", apiTasksDelete)
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary S3 buckets
|
||||
// @Description **Get list of S3 buckets**
|
||||
// @Description
|
||||
// @Description List configured S3 buckets.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of S3 buckets"
|
||||
// @Router /api/s3 [get]
|
||||
func apiS3List(c *gin.Context) {
|
||||
keys := []string{}
|
||||
for k := range context.Config().S3PublishRoots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
}
|
||||
+619
-205
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type SnapshotsSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&SnapshotsSuite{})
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("count-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var snapshots []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &snapshots)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, snapshot := range snapshots {
|
||||
if snapshot["Name"] == "count-snapshot-list" {
|
||||
found = true
|
||||
value, ok := snapshot["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("broken-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, snapshot.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type diskFree struct {
|
||||
// Storage size [MiB]
|
||||
Total uint64
|
||||
// Available Storage [MiB]
|
||||
Free uint64
|
||||
// Percentage Full
|
||||
PercentFull float32
|
||||
}
|
||||
|
||||
// @Summary Get Storage Utilization
|
||||
// @Description **Get disk free information of aptly storage**
|
||||
// @Description
|
||||
// @Description Units in MiB.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} diskFree "Storage information"
|
||||
// @Failure 400 {object} Error "Internal Error"
|
||||
// @Router /api/storage [get]
|
||||
func apiDiskFree(c *gin.Context) {
|
||||
var df diskFree
|
||||
|
||||
fs := context.Config().GetRootDir()
|
||||
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs(fs, &stat)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("Error getting storage info on %s: %s", fs, err))
|
||||
return
|
||||
}
|
||||
|
||||
df.Total = uint64(stat.Blocks) * uint64(stat.Bsize) / 1048576
|
||||
df.Free = uint64(stat.Bavail) * uint64(stat.Bsize) / 1048576
|
||||
df.PercentFull = 100.0 - float32(stat.Bavail)/float32(stat.Blocks)*100.0
|
||||
|
||||
c.JSON(200, df)
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary List Tasks
|
||||
// @Description **Get list of available tasks. Each task is returned as in “show” API**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 {array} task.Task
|
||||
// @Router /api/tasks [get]
|
||||
func apiTasksList(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
c.JSON(200, list.GetTasks())
|
||||
}
|
||||
|
||||
// @Summary Clear Tasks
|
||||
// @Description **Removes finished and failed tasks from internal task list**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/tasks-clear [post]
|
||||
func apiTasksClear(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
list.Clear()
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// @Summary Wait for all Tasks
|
||||
// @Description **Waits for and returns when all running tasks are complete**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/tasks-wait [get]
|
||||
func apiTasksWait(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
list.Wait()
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// @Summary Wait for Task
|
||||
// @Description **Waits for and returns when given Task ID is complete**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad id?"
|
||||
// @Failure 400 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/wait [get]
|
||||
func apiTasksWaitForTaskByID(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
task, err := list.WaitForTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, task)
|
||||
}
|
||||
|
||||
// @Summary Get Task Info
|
||||
// @Description **Return task information for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad id?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id} [get]
|
||||
func apiTasksShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var task task.Task
|
||||
task, err = list.GetTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, task)
|
||||
}
|
||||
|
||||
// @Summary Get Task Output
|
||||
// @Description **Return task output for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "Task output"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/output [get]
|
||||
func apiTasksOutputShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
output, err = list.GetTaskOutputByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, output)
|
||||
}
|
||||
|
||||
// @Summary Get Task Details
|
||||
// @Description **Return task detail for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "Task detail"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/detail [get]
|
||||
func apiTasksDetailShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var detail interface{}
|
||||
detail, err = list.GetTaskDetailByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, detail)
|
||||
}
|
||||
|
||||
// @Summary Get Task Return Value
|
||||
// @Description **Return task return value (status code) by given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/tasks/{id}/return_value [get]
|
||||
func apiTasksReturnValueShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := list.GetTaskReturnValueByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, output)
|
||||
}
|
||||
|
||||
// @Summary Delete Task
|
||||
// @Description **Delete completed task by given ID. Does not stop task execution**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 400 {object} Error "Task in progress or not found"
|
||||
// @Router /api/tasks/{id} [delete]
|
||||
func apiTasksDelete(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var delTask task.Task
|
||||
delTask, err = list.DeleteTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, delTask)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=APT repository API
|
||||
After=network.target
|
||||
Documentation=man:aptly(1)
|
||||
Documentation=https://www.aptly.info/doc/api/
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/aptly serve api -no-lock -listen=127.0.0.1:8081
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=APT repository server
|
||||
After=network.target
|
||||
Documentation=man:aptly(1)
|
||||
Documentation=https://www.aptly.info/doc/commands/
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/aptly serve -listen=127.0.0.1:8080
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,4 @@
|
||||
package aptly
|
||||
|
||||
// AptlyConf holds the default aptly.conf (filled in at link time)
|
||||
var AptlyConf []byte
|
||||
@@ -0,0 +1,3 @@
|
||||
package aptly
|
||||
|
||||
var DistributionFocal = "focal"
|
||||
+43
-5
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
)
|
||||
|
||||
@@ -34,8 +35,8 @@ type PackagePool interface {
|
||||
Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage ChecksumStorage) (path string, err error)
|
||||
// LegacyPath returns legacy (pre 1.1) path to package file (relative to root)
|
||||
LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error)
|
||||
// Stat returns Unix stat(2) info
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
// Size returns the size of the given file in bytes.
|
||||
Size(path string) (size int64, err error)
|
||||
// Open returns ReadSeekerCloser to access the file
|
||||
Open(path string) (ReadSeekerCloser, error)
|
||||
// FilepathList returns file paths of all the files in the pool
|
||||
@@ -46,6 +47,8 @@ type PackagePool interface {
|
||||
|
||||
// LocalPackagePool is implemented by PackagePools residing on the same filesystem
|
||||
type LocalPackagePool interface {
|
||||
// Stat returns Unix stat(2) info
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
// GenerateTempPath generates temporary path for download (which is fast to import into package pool later on)
|
||||
GenerateTempPath(filename string) (string, error)
|
||||
// Link generates hardlink to destination path
|
||||
@@ -69,7 +72,7 @@ type PublishedStorage interface {
|
||||
// Remove removes single file under public path
|
||||
Remove(path string) error
|
||||
// LinkFromPool links package file from pool to dist's pool location
|
||||
LinkFromPool(publishedDirectory, baseName string, sourcePool PackagePool, sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error
|
||||
LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool PackagePool, sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error
|
||||
// Filelist returns list of files under prefix
|
||||
Filelist(prefix string) ([]string, error)
|
||||
// RenameFile renames (moves) file
|
||||
@@ -96,6 +99,36 @@ type PublishedStorageProvider interface {
|
||||
GetPublishedStorage(name string) PublishedStorage
|
||||
}
|
||||
|
||||
// BarType used to differentiate between different progress bars
|
||||
type BarType int
|
||||
|
||||
const (
|
||||
// BarGeneralBuildPackageList identifies bar for building package list
|
||||
BarGeneralBuildPackageList BarType = iota
|
||||
// BarGeneralVerifyDependencies identifies bar for verifying dependencies
|
||||
BarGeneralVerifyDependencies
|
||||
// BarGeneralBuildFileList identifies bar for building file list
|
||||
BarGeneralBuildFileList
|
||||
// BarCleanupBuildList identifies bar for building list to cleanup
|
||||
BarCleanupBuildList
|
||||
// BarCleanupDeleteUnreferencedFiles identifies bar for deleting unreferenced files
|
||||
BarCleanupDeleteUnreferencedFiles
|
||||
// BarMirrorUpdateDownloadIndexes identifies bar for downloading index files
|
||||
BarMirrorUpdateDownloadIndexes
|
||||
// BarMirrorUpdateDownloadPackages identifies bar for downloading packages
|
||||
BarMirrorUpdateDownloadPackages
|
||||
// BarMirrorUpdateBuildPackageList identifies bar for building package list of downloaded files
|
||||
BarMirrorUpdateBuildPackageList
|
||||
// BarMirrorUpdateImportFiles identifies bar for importing package files
|
||||
BarMirrorUpdateImportFiles
|
||||
// BarMirrorUpdateFinalizeDownload identifies bar for finalizing downloads
|
||||
BarMirrorUpdateFinalizeDownload
|
||||
// BarPublishGeneratePackageFiles identifies bar for generating package files to publish
|
||||
BarPublishGeneratePackageFiles
|
||||
// BarPublishFinalizeIndexes identifies bar for finalizing index files
|
||||
BarPublishFinalizeIndexes
|
||||
)
|
||||
|
||||
// Progress is a progress displaying entity, it allows progress bars & simple prints
|
||||
type Progress interface {
|
||||
// Writer interface to support progress bar ticking
|
||||
@@ -107,7 +140,7 @@ type Progress interface {
|
||||
// Flush returns when all queued messages are sent
|
||||
Flush()
|
||||
// InitBar starts progressbar for count bytes or count items
|
||||
InitBar(count int64, isBytes bool)
|
||||
InitBar(count int64, isBytes bool, barType BarType)
|
||||
// ShutdownBar stops progress bar and hides it
|
||||
ShutdownBar()
|
||||
// AddBar increments progress for progress bar
|
||||
@@ -127,11 +160,16 @@ type Downloader interface {
|
||||
// Download starts new download task
|
||||
Download(ctx context.Context, url string, destination string) error
|
||||
// DownloadWithChecksum starts new download task with checksum verification
|
||||
DownloadWithChecksum(ctx context.Context, url string, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool, maxTries int) error
|
||||
DownloadWithChecksum(ctx context.Context, url string, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error
|
||||
// GetProgress returns Progress object
|
||||
GetProgress() Progress
|
||||
// GetLength returns size by heading object with url
|
||||
GetLength(ctx context.Context, url string) (int64, error)
|
||||
}
|
||||
|
||||
// ChecksumStorageProvider creates ChecksumStorage based on DB
|
||||
type ChecksumStorageProvider func(db database.ReaderWriter) ChecksumStorage
|
||||
|
||||
// ChecksumStorage is stores checksums in some (persistent) storage
|
||||
type ChecksumStorage interface {
|
||||
// Get finds checksums in DB by path
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package azure
|
||||
|
||||
// Package azure handles publishing to Azure Storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
)
|
||||
|
||||
func isBlobNotFound(err error) bool {
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
return ok && storageError.ServiceCode() == azblob.ServiceCodeBlobNotFound
|
||||
}
|
||||
|
||||
type azContext struct {
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newAzContext(accountName, accountKey, container, prefix, endpoint string) (*azContext, error) {
|
||||
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/%s", endpoint, container))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerURL := azblob.NewContainerURL(*url, azblob.NewPipeline(credential, azblob.PipelineOptions{}))
|
||||
|
||||
result := &azContext{
|
||||
container: containerURL,
|
||||
prefix: prefix,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (az *azContext) blobPath(path string) string {
|
||||
return filepath.Join(az.prefix, path)
|
||||
}
|
||||
|
||||
func (az *azContext) blobURL(path string) azblob.BlobURL {
|
||||
return az.container.NewBlobURL(az.blobPath(path))
|
||||
}
|
||||
|
||||
func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (paths []string, md5s []string, err error) {
|
||||
const delimiter = "/"
|
||||
paths = make([]string, 0, 1024)
|
||||
md5s = make([]string, 0, 1024)
|
||||
prefix = filepath.Join(az.prefix, prefix)
|
||||
if prefix != "" {
|
||||
prefix += delimiter
|
||||
}
|
||||
|
||||
for marker := (azblob.Marker{}); marker.NotDone(); {
|
||||
listBlob, err := az.container.ListBlobsFlatSegment(
|
||||
context.Background(), marker, azblob.ListBlobsSegmentOptions{
|
||||
Prefix: prefix,
|
||||
MaxResults: 1,
|
||||
Details: azblob.BlobListingDetails{Metadata: true}})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error listing under prefix %s in %s: %s", prefix, az, err)
|
||||
}
|
||||
|
||||
marker = listBlob.NextMarker
|
||||
|
||||
for _, blob := range listBlob.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, blob.Name)
|
||||
} else {
|
||||
paths = append(paths, blob.Name[len(prefix):])
|
||||
}
|
||||
md5s = append(md5s, fmt.Sprintf("%x", blob.Properties.ContentMD5))
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
progress.AddBar(1)
|
||||
}
|
||||
}
|
||||
|
||||
return paths, md5s, nil
|
||||
}
|
||||
|
||||
func (az *azContext) putFile(blob azblob.BlobURL, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := azblob.UploadStreamToBlockBlobOptions{
|
||||
BufferSize: 4 * 1024 * 1024,
|
||||
MaxBuffers: 8,
|
||||
}
|
||||
|
||||
if len(sourceMD5) > 0 {
|
||||
decodedMD5, err := hex.DecodeString(sourceMD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadOptions.BlobHTTPHeaders = azblob.BlobHTTPHeaders{
|
||||
ContentMD5: decodedMD5,
|
||||
}
|
||||
}
|
||||
|
||||
_, err := azblob.UploadStreamToBlockBlob(
|
||||
context.Background(),
|
||||
source,
|
||||
blob.ToBlockBlobURL(),
|
||||
uploadOptions,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// String
|
||||
func (az *azContext) String() string {
|
||||
return fmt.Sprintf("Azure: %s/%s", az.container, az.prefix)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type PackagePool struct {
|
||||
az *azContext
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ aptly.PackagePool = (*PackagePool)(nil)
|
||||
)
|
||||
|
||||
// NewPackagePool creates published storage from Azure storage credentials
|
||||
func NewPackagePool(accountName, accountKey, container, prefix, endpoint string) (*PackagePool, error) {
|
||||
azctx, err := newAzContext(accountName, accountKey, container, prefix, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PackagePool{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String
|
||||
func (pool *PackagePool) String() string {
|
||||
return pool.az.String()
|
||||
}
|
||||
|
||||
func (pool *PackagePool) buildPoolPath(filename string, checksums *utils.ChecksumInfo) string {
|
||||
hash := checksums.SHA256
|
||||
// Use the same path as the file pool, for compat reasons.
|
||||
return filepath.Join(hash[0:2], hash[2:4], hash[4:32]+"_"+filename)
|
||||
}
|
||||
|
||||
func (pool *PackagePool) ensureChecksums(
|
||||
poolPath string,
|
||||
checksumStorage aptly.ChecksumStorage,
|
||||
) (*utils.ChecksumInfo, error) {
|
||||
targetChecksums, err := checksumStorage.Get(poolPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if targetChecksums == nil {
|
||||
// we don't have checksums stored yet for this file
|
||||
blob := pool.az.blobURL(poolPath)
|
||||
download, err := blob.Download(context.Background(), 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "error downloading blob at %s", poolPath)
|
||||
}
|
||||
|
||||
targetChecksums = &utils.ChecksumInfo{}
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body(azblob.RetryReaderOptions{}))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error checksumming blob at %s", poolPath)
|
||||
}
|
||||
|
||||
err = checksumStorage.Update(poolPath, targetChecksums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return targetChecksums, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) FilepathList(progress aptly.Progress) ([]string, error) {
|
||||
if progress != nil {
|
||||
progress.InitBar(0, false, aptly.BarGeneralBuildFileList)
|
||||
defer progress.ShutdownBar()
|
||||
}
|
||||
|
||||
paths, _, err := pool.az.internalFilelist("", progress)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
func (pool *PackagePool) LegacyPath(_ string, _ *utils.ChecksumInfo) (string, error) {
|
||||
return "", errors.New("Azure package pool does not support legacy paths")
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Size(path string) (int64, error) {
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
blob := pool.az.blobURL(path)
|
||||
|
||||
temp, err := os.CreateTemp("", "blob-download")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating temporary file for blob download")
|
||||
}
|
||||
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
err = azblob.DownloadBlobToFile(context.Background(), blob, 0, 0, temp, azblob.DownloadFromBlobOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error downloading blob at %s", path)
|
||||
}
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Remove(path string) (int64, error) {
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error getting props of %s from %s", path, pool)
|
||||
}
|
||||
|
||||
_, err = blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error deleting %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, _ bool, checksumStorage aptly.ChecksumStorage) (string, error) {
|
||||
if checksums.MD5 == "" || checksums.SHA256 == "" || checksums.SHA512 == "" {
|
||||
// need to update checksums, MD5 and SHA256 should be always defined
|
||||
var err error
|
||||
*checksums, err = utils.ChecksumsForFile(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
path := pool.buildPoolPath(basename, checksums)
|
||||
blob := pool.az.blobURL(path)
|
||||
targetChecksums, err := pool.ensureChecksums(path, checksumStorage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if targetChecksums != nil {
|
||||
// target already exists
|
||||
*checksums = *targetChecksums
|
||||
return path, nil
|
||||
}
|
||||
|
||||
source, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = pool.az.putFile(blob, source, checksums.MD5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !checksums.Complete() {
|
||||
// need full checksums here
|
||||
*checksums, err = utils.ChecksumsForFile(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
err = checksumStorage.Update(path, checksums)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, checksumStorage aptly.ChecksumStorage) (string, bool, error) {
|
||||
if poolPath == "" {
|
||||
if checksums.SHA256 != "" {
|
||||
poolPath = pool.buildPoolPath(basename, checksums)
|
||||
} else {
|
||||
// No checksums or pool path, so no idea what file to look for.
|
||||
return "", false, nil
|
||||
}
|
||||
}
|
||||
|
||||
size, err := pool.Size(poolPath)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if size != checksums.Size {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
targetChecksums, err := pool.ensureChecksums(poolPath, checksumStorage)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if targetChecksums == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if checksums.MD5 != "" && targetChecksums.MD5 != checksums.MD5 ||
|
||||
checksums.SHA256 != "" && targetChecksums.SHA256 != checksums.SHA256 {
|
||||
// wrong file?
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// fill back checksums
|
||||
*checksums = *targetChecksums
|
||||
return poolPath, true, nil
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PackagePoolSuite struct {
|
||||
accountName, accountKey, endpoint string
|
||||
pool, prefixedPool *PackagePool
|
||||
debFile string
|
||||
cs aptly.ChecksumStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagePoolSuite{})
|
||||
|
||||
func (s *PackagePoolSuite) SetUpSuite(c *C) {
|
||||
s.accountName = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
if s.accountName == "" {
|
||||
println("Please set the the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCOUNT not set.")
|
||||
}
|
||||
s.accountKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
|
||||
if s.accountKey == "" {
|
||||
println("Please set the the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCESS_KEY not set.")
|
||||
}
|
||||
s.endpoint = os.Getenv("AZURE_STORAGE_ENDPOINT")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) SetUpTest(c *C) {
|
||||
container := randContainer()
|
||||
prefix := "lala"
|
||||
|
||||
var err error
|
||||
|
||||
s.pool, err = NewPackagePool(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
cnt := s.pool.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, _File, _, _ := runtime.Caller(0)
|
||||
s.debFile = filepath.Join(filepath.Dir(_File), "../system/files/libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
s.cs = files.NewMockChecksumStorage()
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
list, err := s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
list, err = s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{
|
||||
"c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb",
|
||||
"c7/6b/4bd12fd92e4dfe1b55b18a67a669_b.deb",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestRemove(c *C) {
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
size, err := s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
_, err = s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
|
||||
list, err := s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"c7/6b/4bd12fd92e4dfe1b55b18a67a669_b.deb"})
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestImportOk(c *C) {
|
||||
var checksum utils.ChecksumInfo
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// SHA256 should be automatically calculated
|
||||
c.Check(checksum.SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
// checksum storage is filled with new checksum
|
||||
c.Check(s.cs.(*files.MockChecksumStorage).Store[path].SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
|
||||
size, err := s.pool.Size(path)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
// import as different name
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, "some.deb", &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_some.deb")
|
||||
// checksum storage is filled with new checksum
|
||||
c.Check(s.cs.(*files.MockChecksumStorage).Store[path].SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
|
||||
// double import, should be ok
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// clear checksum storage, and do double-import
|
||||
delete(s.cs.(*files.MockChecksumStorage).Store, path)
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// checksum is filled back based on re-calculation of file in the pool
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// import under new name, but with path-relevant checksums already filled in
|
||||
checksum = utils.ChecksumInfo{SHA256: checksum.SHA256}
|
||||
path, err = s.pool.Import(s.debFile, "other.deb", &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_other.deb")
|
||||
// checksum is filled back based on re-calculation of source file
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestVerify(c *C) {
|
||||
// file doesn't exist yet
|
||||
ppath, exists, err := s.pool.Verify("", filepath.Base(s.debFile), &utils.ChecksumInfo{}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// import file
|
||||
checksum := utils.ChecksumInfo{}
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
|
||||
// check existence
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, ppath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence with fixed path
|
||||
checksum = utils.ChecksumInfo{Size: checksum.Size}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, but with checksums missing (that aren't needed to find the path)
|
||||
checksum.SHA512 = ""
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with missing checksum info but correct path and size available
|
||||
checksum = utils.ChecksumInfo{Size: checksum.Size}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with wrong checksum info but correct path and size available
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &utils.ChecksumInfo{
|
||||
SHA256: "abc",
|
||||
Size: checksum.Size,
|
||||
}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// check existence, with missing checksums (that aren't needed to find the path)
|
||||
// and no info in checksum storage
|
||||
delete(s.cs.(*files.MockChecksumStorage).Store, path)
|
||||
checksum.SHA512 = ""
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on re-calculation
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with wrong size
|
||||
checksum = utils.ChecksumInfo{Size: 13455}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// check existence, with empty checksum info
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &utils.ChecksumInfo{}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestImportNotExist(c *C) {
|
||||
_, err := s.pool.Import("no-such-file", "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, ErrorMatches, ".*no such file or directory")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestSize(c *C) {
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
size, err := s.pool.Size(path)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
_, err = s.pool.Size("do/es/ntexist")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestOpen(c *C) {
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
f, err := s.pool.Open(path)
|
||||
c.Assert(err, IsNil)
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(contents), Equals, 2738)
|
||||
c.Check(f.Close(), IsNil)
|
||||
|
||||
_, err = s.pool.Open("do/es/ntexist")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PublishedStorage abstract file system with published files (actually hosted on Azure)
|
||||
type PublishedStorage struct {
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
az *azContext
|
||||
pathCache map[string]map[string]string
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
)
|
||||
|
||||
// NewPublishedStorage creates published storage from Azure storage credentials
|
||||
func NewPublishedStorage(accountName, accountKey, container, prefix, endpoint string) (*PublishedStorage, error) {
|
||||
azctx, err := newAzContext(accountName, accountKey, container, prefix, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PublishedStorage{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String
|
||||
func (storage *PublishedStorage) String() string {
|
||||
return storage.az.String()
|
||||
}
|
||||
|
||||
// MkDir creates directory recursively under public path
|
||||
func (storage *PublishedStorage) MkDir(_ string) error {
|
||||
// no op for Azure
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutFile puts file into published storage at specified path
|
||||
func (storage *PublishedStorage) PutFile(path string, sourceFilename string) error {
|
||||
var (
|
||||
source *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
sourceMD5, err := utils.MD5ChecksumForFile(sourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err = os.Open(sourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(storage.az.blobURL(path), source, sourceMD5)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, storage))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveDirs removes directory structure under public path
|
||||
func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
||||
filelist, err := storage.Filelist(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filename := range filelist {
|
||||
blob := storage.az.blobURL(filepath.Join(path, filename))
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting path %s from %s: %s", filename, storage, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes single file under public path
|
||||
func (storage *PublishedStorage) Remove(path string) error {
|
||||
blob := storage.az.blobURL(path)
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error deleting %s from %s: %s", path, storage, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LinkFromPool links package file from pool to dist's pool location
|
||||
//
|
||||
// publishedPrefix is desired prefix for the location in the pool.
|
||||
// publishedRelPath is desired location in pool (like pool/component/liba/libav/)
|
||||
// sourcePool is instance of aptly.PackagePool
|
||||
// sourcePath is filepath to package file in package pool
|
||||
//
|
||||
// LinkFromPool returns relative path for the published file to be included in package index
|
||||
func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool aptly.PackagePool,
|
||||
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
|
||||
relFilePath := filepath.Join(publishedRelPath, fileName)
|
||||
// prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
// FIXME: check how to integrate publishedPrefix:
|
||||
poolPath := storage.az.blobPath(fileName)
|
||||
|
||||
if storage.pathCache == nil {
|
||||
storage.pathCache = make(map[string]map[string]string)
|
||||
}
|
||||
pathCache := storage.pathCache[publishedPrefix]
|
||||
if pathCache == nil {
|
||||
paths, md5s, err := storage.az.internalFilelist(publishedPrefix, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error caching paths under prefix: %s", err)
|
||||
}
|
||||
|
||||
pathCache = make(map[string]string, len(paths))
|
||||
|
||||
for i := range paths {
|
||||
pathCache[paths[i]] = md5s[i]
|
||||
}
|
||||
storage.pathCache[publishedPrefix] = pathCache
|
||||
}
|
||||
|
||||
destinationMD5, exists := pathCache[relFilePath]
|
||||
sourceMD5 := sourceChecksums.MD5
|
||||
|
||||
if exists {
|
||||
if sourceMD5 == "" {
|
||||
return fmt.Errorf("unable to compare object, MD5 checksum missing")
|
||||
}
|
||||
|
||||
if destinationMD5 == sourceMD5 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !force && destinationMD5 != sourceMD5 {
|
||||
return fmt.Errorf("error putting file to %s: file already exists and is different: %s", poolPath, storage)
|
||||
}
|
||||
}
|
||||
|
||||
source, err := sourcePool.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(storage.az.blobURL(relFilePath), source, sourceMD5)
|
||||
if err == nil {
|
||||
pathCache[relFilePath] = sourceMD5
|
||||
} else {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s: %s", sourcePath, storage, poolPath))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Filelist returns list of files under prefix
|
||||
func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
paths, _, err := storage.az.internalFilelist(prefix, nil)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
// Internal copy or move implementation
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata azblob.Metadata, move bool) error {
|
||||
const leaseDuration = 30
|
||||
|
||||
dstBlobURL := storage.az.blobURL(dst)
|
||||
srcBlobURL := storage.az.blobURL(src)
|
||||
leaseResp, err := srcBlobURL.AcquireLease(context.Background(), "", leaseDuration, azblob.ModifiedAccessConditions{})
|
||||
if err != nil || leaseResp.StatusCode() != http.StatusCreated {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", srcBlobURL)
|
||||
}
|
||||
defer srcBlobURL.BreakLease(context.Background(), azblob.LeaseBreakNaturally, azblob.ModifiedAccessConditions{})
|
||||
srcBlobLeaseID := leaseResp.LeaseID()
|
||||
|
||||
copyResp, err := dstBlobURL.StartCopyFromURL(
|
||||
context.Background(),
|
||||
srcBlobURL.URL(),
|
||||
metadata,
|
||||
azblob.ModifiedAccessConditions{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", src, dst, storage, err)
|
||||
}
|
||||
|
||||
copyStatus := copyResp.CopyStatus()
|
||||
for {
|
||||
if copyStatus == azblob.CopyStatusSuccess {
|
||||
if move {
|
||||
_, err = srcBlobURL.Delete(
|
||||
context.Background(),
|
||||
azblob.DeleteSnapshotsOptionNone,
|
||||
azblob.BlobAccessConditions{
|
||||
LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if copyStatus == azblob.CopyStatusPending {
|
||||
time.Sleep(1 * time.Second)
|
||||
blobPropsResp, err := dstBlobURL.GetProperties(
|
||||
context.Background(),
|
||||
azblob.BlobAccessConditions{LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID}},
|
||||
azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting destination blob properties %s", dstBlobURL)
|
||||
}
|
||||
copyStatus = blobPropsResp.CopyStatus()
|
||||
|
||||
_, err = srcBlobURL.RenewLease(context.Background(), srcBlobLeaseID, azblob.ModifiedAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error renewing source blob lease %s", srcBlobURL)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", dst, src, storage, copyStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RenameFile renames (moves) file
|
||||
func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
return storage.internalCopyOrMoveBlob(oldName, newName, nil, true /* move */)
|
||||
}
|
||||
|
||||
// SymLink creates a copy of src file and adds link information as meta data
|
||||
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
||||
return storage.internalCopyOrMoveBlob(src, dst, azblob.Metadata{"SymLink": src}, false /* move */)
|
||||
}
|
||||
|
||||
// HardLink using symlink functionality as hard links do not exist
|
||||
func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
||||
return storage.SymLink(src, dst)
|
||||
}
|
||||
|
||||
// FileExists returns true if path exists
|
||||
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
} else if resp.StatusCode() == http.StatusOK {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
|
||||
// ReadLink returns the symbolic link pointed to by path.
|
||||
// This simply reads text file created with SymLink
|
||||
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if resp.StatusCode() != http.StatusOK {
|
||||
return "", fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
return resp.NewMetadata()["SymLink"], nil
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
accountName, accountKey, endpoint string
|
||||
storage, prefixedStorage *PublishedStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishedStorageSuite{})
|
||||
|
||||
const testContainerPrefix = "aptlytest-"
|
||||
|
||||
func randContainer() string {
|
||||
return testContainerPrefix + randString(32-len(testContainerPrefix))
|
||||
}
|
||||
|
||||
func randString(n int) string {
|
||||
if n <= 0 {
|
||||
panic("negative number")
|
||||
}
|
||||
const alphanum = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
var bytes = make([]byte, n)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpSuite(c *C) {
|
||||
s.accountName = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
if s.accountName == "" {
|
||||
println("Please set the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCOUNT not set.")
|
||||
}
|
||||
s.accountKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
|
||||
if s.accountKey == "" {
|
||||
println("Please set the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCESS_KEY not set.")
|
||||
}
|
||||
s.endpoint = os.Getenv("AZURE_STORAGE_ENDPOINT")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
container := randContainer()
|
||||
prefix := "lala"
|
||||
|
||||
var err error
|
||||
|
||||
s.storage, err = NewPublishedStorage(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
cnt := s.storage.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
cnt := s.storage.az.container
|
||||
_, err := cnt.Delete(context.Background(), azblob.ContainerAccessConditions{})
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
blob := s.storage.az.container.NewBlobURL(path)
|
||||
resp, err := blob.Download(context.Background(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, IsNil)
|
||||
body := resp.Body(azblob.RetryReaderOptions{MaxRetryRequests: 3})
|
||||
data, err := ioutil.ReadAll(body)
|
||||
c.Assert(err, IsNil)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
_, err := s.storage.az.container.NewBlobURL(path).GetProperties(
|
||||
context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, NotNil)
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(string(storageError.ServiceCode()), Equals, string(string(azblob.StorageErrorCodeBlobNotFound)))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
hash := md5.Sum(data)
|
||||
_, err := azblob.UploadBufferToBlockBlob(
|
||||
context.Background(),
|
||||
data,
|
||||
s.storage.az.container.NewBlockBlobURL(path),
|
||||
azblob.UploadToBlockBlobOptions{
|
||||
BlobHTTPHeaders: azblob.BlobHTTPHeaders{
|
||||
ContentMD5: hash[:],
|
||||
},
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
content := []byte("Welcome to Azure!")
|
||||
filename := "a/b.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filename), DeepEquals, content)
|
||||
|
||||
err = s.prefixedStorage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filepath.Join(s.prefixedStorage.az.prefix, filename)), DeepEquals, content)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFilePlus(c *C) {
|
||||
content := []byte("Welcome to Azure!")
|
||||
filename := "a/b+c.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filename), DeepEquals, content)
|
||||
s.AssertNoFile(c, "a/b c.txt")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "test/a", "test/b", "testa"})
|
||||
|
||||
list, err = s.storage.Filelist("test")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b"})
|
||||
|
||||
list, err = s.storage.Filelist("test2")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
list, err = s.prefixedStorage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelistPlus(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a+1", "test/a 1", "lala/a+b", "lala/a b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a b", "lala/a+b", "lala/c", "test/a 1", "test/a+1", "testa"})
|
||||
|
||||
list, err = s.storage.Filelist("test")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a 1", "a+1"})
|
||||
|
||||
list, err = s.storage.Filelist("test2")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
list, err = s.prefixedStorage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a b", "a+b", "c"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.Remove("a/b")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b")
|
||||
|
||||
s.PutFile(c, "lala/xyz", []byte("test"))
|
||||
|
||||
err = s.prefixedStorage.Remove("xyz")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "lala/xyz")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemovePlus(c *C) {
|
||||
s.PutFile(c, "a/b+c", []byte("test"))
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.Remove("a/b+c")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b+c")
|
||||
s.AssertNoFile(c, "a/b c")
|
||||
|
||||
err = s.storage.Remove("a/b")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirs(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
err := s.storage.RemoveDirs("test", nil)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "testa"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsPlus(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a+1", "test/a 1", "lala/a+b", "lala/a b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
err := s.storage.RemoveDirs("test", nil)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a b", "lala/a+b", "lala/c", "testa"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
dir := c.MkDir()
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("source.txt", filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = s.storage.RenameFile("source.txt", "dest.txt")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "dest.txt"), DeepEquals, []byte("Welcome to Azure!"))
|
||||
|
||||
exists, err := s.storage.FileExists("source.txt")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
root := c.MkDir()
|
||||
pool := files.NewPackagePool(root, false)
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err := ioutil.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum1 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err = ioutil.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum2 := utils.ChecksumInfo{MD5: "e9dfd31cc505d51fc26975250750deab"}
|
||||
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "netboot/boot.img.gz")
|
||||
os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = ioutil.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum3 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
src1, err := pool.Import(tmpFile1, "mars-invaders_1.03.deb", &cksum1, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src2, err := pool.Import(tmpFile2, "mars-invaders_1.03.deb", &cksum2, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src3, err := pool.Import(tmpFile3, "netboot/boot.img.gz", &cksum3, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// first link from pool
|
||||
err = s.storage.LinkFromPool("", filepath.Join("", "pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// duplicate link from pool
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, false)
|
||||
c.Check(err, ErrorMatches, ".*file already exists and is different.*")
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict and force
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, true)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Spam"))
|
||||
|
||||
// for prefixed storage:
|
||||
// first link from pool
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// 2nd link from pool, providing wrong path for source file
|
||||
//
|
||||
// this test should check that file already exists in S3 and skip upload (which would fail if not skipped)
|
||||
s.prefixedStorage.pathCache = nil
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, "wrong-looks-like-pathcache-doesnt-work", cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "lala/pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with nested file name
|
||||
err = s.storage.LinkFromPool("", "dists/jessie/non-free/installer-i386/current/images", "netboot/boot.img.gz", pool, src3, cksum3, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "dists/jessie/non-free/installer-i386/current/images/netboot/boot.img.gz"), DeepEquals, []byte("Contents"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestSymLink(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.SymLink("a/b", "a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
var link string
|
||||
link, err = s.storage.ReadLink("a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(link, Equals, "a/b")
|
||||
|
||||
c.Skip("copy not available in azure test")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFileExists(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
exists, err := s.storage.FileExists("a/b")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
|
||||
exists, _ = s.storage.FileExists("a/b.invalid")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
+27
-12
@@ -1,11 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/api"
|
||||
"github.com/aptly-dev/aptly/systemd/activation"
|
||||
@@ -30,7 +34,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
// anything else must fail.
|
||||
// E.g.: Running the service under a different user may lead to a rootDir
|
||||
// that exists but is not usable due to access permissions.
|
||||
err = utils.DirIsAccessible(context.Config().RootDir)
|
||||
err = utils.DirIsAccessible(context.Config().GetRootDir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,7 +46,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
if err == nil && len(listeners) == 1 {
|
||||
listener := listeners[0]
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
|
||||
err = http.Serve(listener, api.Router(context))
|
||||
if err != nil {
|
||||
@@ -55,31 +59,42 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
listen := context.Flags().Lookup("listen").Value.String()
|
||||
fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen)
|
||||
|
||||
server := http.Server{Handler: api.Router(context)}
|
||||
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go (func() {
|
||||
if _, ok := <-sigchan; ok {
|
||||
fmt.Printf("\nShutdown signal received, waiting for background tasks...\n")
|
||||
context.TaskList().Wait()
|
||||
_ = server.Shutdown(stdcontext.Background())
|
||||
}
|
||||
})()
|
||||
defer close(sigchan)
|
||||
|
||||
listenURL, err := url.Parse(listen)
|
||||
if err == nil && listenURL.Scheme == "unix" {
|
||||
file := listenURL.Path
|
||||
os.Remove(file)
|
||||
_ = os.Remove(file)
|
||||
|
||||
var listener net.Listener
|
||||
listener, err = net.Listen("unix", file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on: %s\n%s", file, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
err = http.Serve(listener, api.Router(context))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to serve: %s", err)
|
||||
}
|
||||
return nil
|
||||
err = server.Serve(listener)
|
||||
} else {
|
||||
server.Addr = listen
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
|
||||
err = http.ListenAndServe(listen, api.Router(context))
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("unable to serve: %s", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeCmdAPIServe() *commander.Command {
|
||||
|
||||
+5
-5
@@ -21,14 +21,14 @@ const (
|
||||
)
|
||||
|
||||
// ListPackagesRefList shows list of packages in PackageRefList
|
||||
func ListPackagesRefList(reflist *deb.PackageRefList) (err error) {
|
||||
func ListPackagesRefList(reflist *deb.PackageRefList, collectionFactory *deb.CollectionFactory) (err error) {
|
||||
fmt.Printf("Packages:\n")
|
||||
|
||||
if reflist == nil {
|
||||
return
|
||||
}
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(reflist, context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ package environment to new version.`,
|
||||
Flag: *flag.NewFlagSet("aptly", flag.ExitOnError),
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdConfig(),
|
||||
makeCmdDb(),
|
||||
makeCmdDB(),
|
||||
makeCmdGraph(),
|
||||
makeCmdMirror(),
|
||||
makeCmdRepo(),
|
||||
@@ -118,8 +118,8 @@ package environment to new version.`,
|
||||
cmd.Flag.Bool("dep-follow-all-variants", false, "when processing dependencies, follow a & b if dependency is 'a|b'")
|
||||
cmd.Flag.Bool("dep-verbose-resolve", false, "when processing dependencies, print detailed logs")
|
||||
cmd.Flag.String("architectures", "", "list of architectures to consider during (comma-separated), default to all available")
|
||||
cmd.Flag.String("config", "", "location of configuration file (default locations are /etc/aptly.conf, ~/.aptly.conf)")
|
||||
cmd.Flag.String("gpg-provider", "", "PGP implementation (\"gpg\" for external gpg or \"internal\" for Go internal implementation)")
|
||||
cmd.Flag.String("config", "", "location of configuration file (default locations in order: ~/.aptly.conf, /usr/local/etc/aptly.conf, /etc/aptly.conf)")
|
||||
cmd.Flag.String("gpg-provider", "", "PGP implementation (\"gpg\", \"gpg1\", \"gpg2\" for external gpg or \"internal\" for Go internal implementation)")
|
||||
|
||||
if aptly.EnableDebug {
|
||||
cmd.Flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
+18
-6
@@ -5,19 +5,30 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smira/commander"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func aptlyConfigShow(cmd *commander.Command, args []string) error {
|
||||
func aptlyConfigShow(_ *commander.Command, _ []string) error {
|
||||
showYaml := context.Flags().Lookup("yaml").Value.Get().(bool)
|
||||
|
||||
config := context.Config()
|
||||
prettyJSON, err := json.MarshalIndent(config, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to dump the config file: %s", err)
|
||||
if showYaml {
|
||||
yamlData, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling to YAML: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(yamlData))
|
||||
} else {
|
||||
prettyJSON, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to dump the config file: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(prettyJSON))
|
||||
}
|
||||
|
||||
fmt.Println(string(prettyJSON))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,5 +46,6 @@ Example:
|
||||
|
||||
`,
|
||||
}
|
||||
cmd.Flag.Bool("yaml", false, "show yaml config")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdDb() *commander.Command {
|
||||
func makeCmdDB() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "db",
|
||||
Short: "manage aptly's internal database and package pool",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdDbCleanup(),
|
||||
makeCmdDbRecover(),
|
||||
makeCmdDBCleanup(),
|
||||
makeCmdDBRecover(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+46
-23
@@ -5,13 +5,14 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
// aptly db cleanup
|
||||
func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -21,9 +22,11 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
verbose := context.Flags().Lookup("verbose").Value.Get().(bool)
|
||||
dryRun := context.Flags().Lookup("dry-run").Value.Get().(bool)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// collect information about references packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
referencedAppStreamFiles := []string{}
|
||||
|
||||
// used only in verbose mode to report package use source
|
||||
packageRefSources := map[string][]string{}
|
||||
@@ -32,12 +35,12 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading mirrors:@|")
|
||||
}
|
||||
err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", repo.Name)
|
||||
}
|
||||
|
||||
e := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -46,28 +49,34 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("mirror %s", repo.Name)
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, poolPath := range repo.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading local repos:@|")
|
||||
}
|
||||
err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", repo.Name)
|
||||
}
|
||||
|
||||
e := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -77,7 +86,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("local repo %s", repo.Name)
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -90,15 +99,17 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading snapshots:@|")
|
||||
}
|
||||
err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", snapshot.Name)
|
||||
}
|
||||
|
||||
e := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -107,28 +118,35 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("snapshot %s", snapshot.Name)
|
||||
snapshot.RefList().ForEach(func(key []byte) error {
|
||||
_ = snapshot.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for _, poolPath := range snapshot.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading published repositories:@|")
|
||||
}
|
||||
err = context.CollectionFactory().PublishedRepoCollection().ForEach(func(published *deb.PublishedRepo) error {
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(published *deb.PublishedRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s:%s/%s{|}", published.Storage, published.Prefix, published.Distribution)
|
||||
}
|
||||
if published.SourceKind != deb.SourceLocalRepo {
|
||||
return nil
|
||||
}
|
||||
e := context.CollectionFactory().PublishedRepoCollection().LoadComplete(published, context.CollectionFactory())
|
||||
e := collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -138,7 +156,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
if verbose {
|
||||
description := fmt.Sprintf("published repository %s:%s/%s component %s",
|
||||
published.Storage, published.Prefix, published.Distribution, component)
|
||||
published.RefList(component).ForEach(func(key []byte) error {
|
||||
_ = published.RefList(component).ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -150,9 +168,11 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
// ... and compare it to the list of all packages
|
||||
context.Progress().ColoredPrintf("@{w!}Loading list of all packages...@|")
|
||||
allPackageRefs := context.CollectionFactory().PackageCollection().AllPackageRefs()
|
||||
allPackageRefs := collectionFactory.PackageCollection().AllPackageRefs()
|
||||
|
||||
toDelete := allPackageRefs.Subtract(existingPackageRefs)
|
||||
|
||||
@@ -175,15 +195,15 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
db.StartBatch()
|
||||
batch := db.CreateBatch()
|
||||
err = toDelete.ForEach(func(ref []byte) error {
|
||||
return context.CollectionFactory().PackageCollection().DeleteByKey(ref)
|
||||
return collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to delete by key: %s", err)
|
||||
}
|
||||
|
||||
err = db.FinishBatch()
|
||||
err = batch.Write()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to DB: %s", err)
|
||||
}
|
||||
@@ -192,13 +212,15 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
// now, build a list of files that should be present in Repository (package pool)
|
||||
context.Progress().ColoredPrintf("@{w!}Building list of files referenced by packages...@|")
|
||||
referencedFiles := make([]string, 0, existingPackageRefs.Len())
|
||||
context.Progress().InitBar(int64(existingPackageRefs.Len()), false)
|
||||
context.Progress().InitBar(int64(existingPackageRefs.Len()), false, aptly.BarCleanupBuildList)
|
||||
|
||||
err = existingPackageRefs.ForEach(func(key []byte) error {
|
||||
pkg, err2 := context.CollectionFactory().PackageCollection().ByKey(key)
|
||||
pkg, err2 := collectionFactory.PackageCollection().ByKey(key)
|
||||
if err2 != nil {
|
||||
tail := ""
|
||||
if verbose {
|
||||
@@ -224,6 +246,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
referencedFiles = append(referencedFiles, referencedAppStreamFiles...)
|
||||
sort.Strings(referencedFiles)
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
@@ -249,7 +272,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
context.Progress().InitBar(int64(len(filesToDelete)), false)
|
||||
context.Progress().InitBar(int64(len(filesToDelete)), false, aptly.BarCleanupDeleteUnreferencedFiles)
|
||||
|
||||
var size, totalSize int64
|
||||
for _, file := range filesToDelete {
|
||||
@@ -279,9 +302,9 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbCleanup() *commander.Command {
|
||||
func makeCmdDBCleanup() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbCleanup,
|
||||
Run: aptlyDBCleanup,
|
||||
UsageLine: "cleanup",
|
||||
Short: "cleanup DB and package pool",
|
||||
Long: `
|
||||
|
||||
+47
-5
@@ -1,12 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
// aptly db recover
|
||||
func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBRecover(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -15,14 +19,19 @@ func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
context.Progress().Printf("Recovering database...\n")
|
||||
err = database.RecoverDB(context.DBPath())
|
||||
if err = goleveldb.RecoverDB(context.DBPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.Progress().Printf("Checking database integrity...\n")
|
||||
err = checkIntegrity()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbRecover() *commander.Command {
|
||||
func makeCmdDBRecover() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbRecover,
|
||||
Run: aptlyDBRecover,
|
||||
UsageLine: "recover",
|
||||
Short: "recover DB after crash",
|
||||
Long: `
|
||||
@@ -37,3 +46,36 @@ Example:
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func checkIntegrity() error {
|
||||
return context.NewCollectionFactory().LocalRepoCollection().ForEach(checkRepo)
|
||||
}
|
||||
|
||||
func checkRepo(repo *deb.LocalRepo) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := collectionFactory.LocalRepoCollection()
|
||||
|
||||
err := repos.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load complete repo %q: %s", repo.Name, err)
|
||||
}
|
||||
|
||||
dangling, err := deb.FindDanglingReferences(repo.RefList(), collectionFactory.PackageCollection())
|
||||
if err != nil {
|
||||
return fmt.Errorf("find dangling references: %w", err)
|
||||
}
|
||||
|
||||
if len(dangling.Refs) > 0 {
|
||||
for _, ref := range dangling.Refs {
|
||||
context.Progress().Printf("Removing dangling database reference %q\n", ref)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(repo.RefList().Subtract(dangling))
|
||||
|
||||
if err = repos.Update(repo); err != nil {
|
||||
return fmt.Errorf("update repo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+8
-21
@@ -4,13 +4,11 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -28,20 +26,20 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
layout := context.Flags().Lookup("layout").Value.String()
|
||||
|
||||
fmt.Printf("Generating graph...\n")
|
||||
graph, err := deb.BuildGraph(context.CollectionFactory(), layout)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
graph, err := deb.BuildGraph(collectionFactory, layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString(graph.String())
|
||||
|
||||
tempfile, err := ioutil.TempFile("", "aptly-graph")
|
||||
tempfile, err := os.CreateTemp("", "aptly-graph")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempfile.Close()
|
||||
os.Remove(tempfile.Name())
|
||||
_ = tempfile.Close()
|
||||
_ = os.Remove(tempfile.Name())
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
output := context.Flags().Lookup("output").Value.String()
|
||||
@@ -80,10 +78,6 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = os.Remove(tempfilename)
|
||||
}()
|
||||
|
||||
if output != "" {
|
||||
err = utils.CopyFile(tempfilename, output)
|
||||
if err != nil {
|
||||
@@ -91,23 +85,16 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
fmt.Printf("Output saved to %s\n", output)
|
||||
_ = os.Remove(tempfilename)
|
||||
} else {
|
||||
command := getOpenCommand()
|
||||
fmt.Printf("Rendered to %s file: %s, trying to open it with: %s %s...\n", format, tempfilename, command, tempfilename)
|
||||
fmt.Printf("Displaying %s file: %s %s\n", format, command, tempfilename)
|
||||
|
||||
args := strings.Split(command, " ")
|
||||
|
||||
viewer := exec.Command(args[0], append(args[1:], tempfilename)...)
|
||||
viewer.Stderr = os.Stderr
|
||||
if err = viewer.Start(); err == nil {
|
||||
// Wait for a second so that the visualizer has a chance to
|
||||
// open the input file. This needs to be done even if we're
|
||||
// waiting for the visualizer as it can be just a wrapper that
|
||||
// spawns a browser tab and returns right away.
|
||||
defer func(t <-chan time.Time) {
|
||||
<-t
|
||||
}(time.After(time.Second))
|
||||
}
|
||||
err = viewer.Start()
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
+5
-5
@@ -9,18 +9,18 @@ import (
|
||||
)
|
||||
|
||||
func getVerifier(flags *flag.FlagSet) (pgp.Verifier, error) {
|
||||
if LookupOption(context.Config().GpgDisableVerify, flags, "ignore-signatures") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
keyRings := flags.Lookup("keyring").Value.Get().([]string)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
|
||||
verifier := context.GetVerifier()
|
||||
for _, keyRing := range keyRings {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring()
|
||||
err := verifier.InitKeyring(!ignoreSignatures) // be verbose only if verifying signatures is requested
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+15
-5
@@ -19,6 +19,12 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
|
||||
downloadSources := LookupOption(context.Config().DownloadSourcePackages, context.Flags(), "with-sources")
|
||||
downloadUdebs := context.Flags().Lookup("with-udebs").Value.Get().(bool)
|
||||
downloadInstaller := context.Flags().Lookup("with-installer").Value.Get().(bool)
|
||||
downloadAppStream := context.Flags().Lookup("with-appstream").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
|
||||
var (
|
||||
mirrorName, archiveURL, distribution string
|
||||
@@ -36,12 +42,12 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(mirrorName, archiveURL, distribution, components, context.ArchitecturesList(),
|
||||
downloadSources, downloadUdebs)
|
||||
downloadSources, downloadUdebs, downloadInstaller, downloadAppStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create mirror: %s", err)
|
||||
}
|
||||
|
||||
repo.Filter = context.Flags().Lookup("filter").Value.String()
|
||||
repo.Filter = context.Flags().Lookup("filter").Value.String() // allows file/stdin with @
|
||||
repo.FilterWithDeps = context.Flags().Lookup("filter-with-deps").Value.Get().(bool)
|
||||
repo.SkipComponentCheck = context.Flags().Lookup("force-components").Value.Get().(bool)
|
||||
repo.SkipArchitectureCheck = context.Flags().Lookup("force-architectures").Value.Get().(bool)
|
||||
@@ -58,12 +64,13 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier)
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch mirror: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Add(repo)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
err = collectionFactory.RemoteRepoCollection().Add(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add mirror: %s", err)
|
||||
}
|
||||
@@ -94,12 +101,15 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
cmd.Flag.String("filter", "", "filter packages in mirror")
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("force-components", false, "(only with component list) skip check that requested components are listed in Release file")
|
||||
cmd.Flag.Bool("force-architectures", false, "(only with architecture list) skip check that requested architectures are listed in Release file")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
return cmd
|
||||
|
||||
+4
-3
@@ -15,8 +15,9 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
@@ -28,7 +29,7 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
|
||||
force := context.Flags().Lookup("force").Value.Get().(bool)
|
||||
if !force {
|
||||
snapshots := context.CollectionFactory().SnapshotCollection().ByRemoteRepoSource(repo)
|
||||
snapshots := collectionFactory.SnapshotCollection().ByRemoteRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
fmt.Printf("Mirror `%s` was used to create following snapshots:\n", repo.Name)
|
||||
@@ -40,7 +41,7 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Drop(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Drop(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
+19
-5
@@ -16,7 +16,8 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(args[0])
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
@@ -27,12 +28,17 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
fetchMirror := false
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
context.Flags().Visit(func(flag *flag.Flag) {
|
||||
switch flag.Name {
|
||||
case "filter":
|
||||
repo.Filter = flag.Value.String()
|
||||
repo.Filter = flag.Value.String() // allows file/stdin with @
|
||||
case "filter-with-deps":
|
||||
repo.FilterWithDeps = flag.Value.Get().(bool)
|
||||
case "with-appstream":
|
||||
repo.DownloadAppStream = flag.Value.Get().(bool)
|
||||
case "with-installer":
|
||||
repo.DownloadInstaller = flag.Value.Get().(bool)
|
||||
case "with-sources":
|
||||
repo.DownloadSources = flag.Value.Get().(bool)
|
||||
case "with-udebs":
|
||||
@@ -40,6 +46,8 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
case "archive-url":
|
||||
repo.SetArchiveRoot(flag.Value.String())
|
||||
fetchMirror = true
|
||||
case "ignore-signatures":
|
||||
ignoreSignatures = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,6 +55,10 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support udebs")
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadAppStream {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support AppStream (DEP-11) metadata")
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
_, err = query.Parse(repo.Filter)
|
||||
if err != nil {
|
||||
@@ -66,13 +78,13 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier)
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
@@ -98,9 +110,11 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.String("archive-url", "", "archive url is the root of archive")
|
||||
cmd.Flag.String("filter", "", "filter packages in mirror")
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
+45
-5
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
@@ -9,17 +10,29 @@ import (
|
||||
)
|
||||
|
||||
func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
repos := make([]string, context.CollectionFactory().RemoteRepoCollection().Len())
|
||||
if jsonFlag {
|
||||
return aptlyMirrorListJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyMirrorListTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repos := make([]string, collectionFactory.RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
_ = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -29,7 +42,7 @@ func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -52,6 +65,32 @@ func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyMirrorListJSON(_ *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
repos := make([]*deb.RemoteRepo, context.NewCollectionFactory().RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
_ = context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
repos[i] = repo
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
})
|
||||
|
||||
if output, e := json.MarshalIndent(repos, "", " "); e == nil {
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdMirrorList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyMirrorList,
|
||||
@@ -66,6 +105,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display list in JSON format")
|
||||
cmd.Flag.Bool("raw", false, "display list in machine-readable format")
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -20,7 +20,8 @@ func aptlyMirrorRename(cmd *commander.Command, args []string) error {
|
||||
|
||||
oldName, newName := args[0], args[1]
|
||||
|
||||
repo, err = context.CollectionFactory().RemoteRepoCollection().ByName(oldName)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err = collectionFactory.RemoteRepoCollection().ByName(oldName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
@@ -30,13 +31,13 @@ func aptlyMirrorRename(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
_, err = context.CollectionFactory().RemoteRepoCollection().ByName(newName)
|
||||
_, err = collectionFactory.RemoteRepoCollection().ByName(newName)
|
||||
if err == nil {
|
||||
return fmt.Errorf("unable to rename: mirror %s already exists", newName)
|
||||
}
|
||||
|
||||
repo.Name = newName
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
+67
-4
@@ -1,7 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
@@ -11,20 +13,32 @@ import (
|
||||
)
|
||||
|
||||
func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyMirrorShowJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyMirrorShowTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
@@ -47,6 +61,11 @@ func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
downloadUdebs = Yes
|
||||
}
|
||||
fmt.Printf("Download .udebs: %s\n", downloadUdebs)
|
||||
downloadAppStream := No
|
||||
if repo.DownloadAppStream {
|
||||
downloadAppStream = Yes
|
||||
}
|
||||
fmt.Printf("Download AppStream: %s\n", downloadAppStream)
|
||||
if repo.Filter != "" {
|
||||
fmt.Printf("Filter: %s\n", repo.Filter)
|
||||
filterWithDeps := No
|
||||
@@ -72,13 +91,56 @@ func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
fmt.Printf("Unable to show package list, mirror hasn't been downloaded yet.\n")
|
||||
} else {
|
||||
ListPackagesRefList(repo.RefList())
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyMirrorShowJSON(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.NewCollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
// include packages if requested
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
if repo.RefList() != nil {
|
||||
var list *deb.PackageList
|
||||
list, err = deb.NewPackageListFromRefList(repo.RefList(), context.NewCollectionFactory().PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get package list: %s", err)
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
repo.Packages = append(repo.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
sort.Strings(repo.Packages)
|
||||
}
|
||||
}
|
||||
|
||||
var output []byte
|
||||
if output, err = json.MarshalIndent(repo, "", " "); err == nil {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdMirrorShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyMirrorShow,
|
||||
@@ -94,6 +156,7 @@ Example:
|
||||
Flag: *flag.NewFlagSet("aptly-mirror-show", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
cmd.Flag.Bool("with-packages", false, "show detailed list of packages and versions stored in the mirror")
|
||||
|
||||
return cmd
|
||||
|
||||
+56
-19
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -22,12 +23,13 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -40,25 +42,37 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
ignoreMismatch := context.Flags().Lookup("ignore-checksums").Value.Get().(bool)
|
||||
maxTries := context.Flags().Lookup("max-tries").Value.Get().(int)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
ignoreChecksums := context.Flags().Lookup("ignore-checksums").Value.Get().(bool)
|
||||
|
||||
verifier, err := getVerifier(context.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier)
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Downloading & parsing package files...\n")
|
||||
err = repo.DownloadPackageIndexes(context.Progress(), context.Downloader(), context.CollectionFactory(), ignoreMismatch, maxTries)
|
||||
err = repo.DownloadPackageIndexes(context.Progress(), context.Downloader(), verifier, collectionFactory, ignoreSignatures, ignoreChecksums)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if repo.DownloadAppStream && !repo.IsFlat() {
|
||||
context.Progress().Printf("Downloading AppStream metadata...\n")
|
||||
err = repo.DownloadAppStreamFiles(context.Progress(), context.Downloader(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection(nil), ignoreChecksums)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
context.Progress().Printf("Applying filter...\n")
|
||||
var filterQuery deb.PackageQuery
|
||||
@@ -82,10 +96,11 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
skipExistingPackages := context.Flags().Lookup("skip-existing-packages").Value.Get().(bool)
|
||||
latestOnly := context.Flags().Lookup("latest").Value.Get().(bool)
|
||||
|
||||
context.Progress().Printf("Building download queue...\n")
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool(), context.CollectionFactory().PackageCollection(),
|
||||
context.CollectionFactory().ChecksumCollection(), skipExistingPackages)
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages, latestOnly)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
@@ -96,12 +111,12 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
err = context.ReOpenDatabase()
|
||||
if err == nil {
|
||||
repo.MarkAsIdle()
|
||||
context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
_ = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
}
|
||||
}()
|
||||
|
||||
repo.MarkAsUpdating()
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -117,7 +132,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
context.Progress().Printf("Download queue: %d items (%s)\n", count, utils.HumanBytes(downloadSize))
|
||||
|
||||
// Download from the queue
|
||||
context.Progress().InitBar(downloadSize, true)
|
||||
context.Progress().InitBar(downloadSize, true, aptly.BarMirrorUpdateDownloadPackages)
|
||||
|
||||
downloadQueue := make(chan int)
|
||||
|
||||
@@ -161,7 +176,16 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
var e error
|
||||
|
||||
// provision download location
|
||||
task.TempDownPath, e = context.PackagePool().(aptly.LocalPackagePool).GenerateTempPath(task.File.Filename)
|
||||
if pp, ok := context.PackagePool().(aptly.LocalPackagePool); ok {
|
||||
task.TempDownPath, e = pp.GenerateTempPath(task.File.Filename)
|
||||
} else {
|
||||
var file *os.File
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
@@ -173,8 +197,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
repo.PackageURL(task.File.DownloadURL()).String(),
|
||||
task.TempDownPath,
|
||||
&task.File.Checksums,
|
||||
ignoreMismatch,
|
||||
maxTries)
|
||||
ignoreChecksums)
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
@@ -198,8 +221,20 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, task := range queue {
|
||||
if task.TempDownPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(task.TempDownPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Failed to delete %s: %v\n", task.TempDownPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Import downloaded files
|
||||
context.Progress().InitBar(int64(len(queue)), false)
|
||||
context.Progress().InitBar(int64(len(queue)), false, aptly.BarMirrorUpdateImportFiles)
|
||||
|
||||
for idx := range queue {
|
||||
context.Progress().AddBar(1)
|
||||
@@ -212,7 +247,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
// and import it back to the pool
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, context.CollectionFactory().ChecksumCollection())
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, collectionFactory.ChecksumCollection(nil))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import file: %s", err)
|
||||
}
|
||||
@@ -236,13 +271,13 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
repo.FinalizeDownload(context.CollectionFactory(), context.Progress())
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
_ = repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nMirror `%s` has been successfully updated.\n", repo.Name)
|
||||
context.Progress().Printf("\nMirror `%s` has been updated successfully.\n", repo.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -267,7 +302,9 @@ Example:
|
||||
cmd.Flag.Bool("ignore-checksums", false, "ignore checksum mismatches while downloading package files and metadata")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("skip-existing-packages", false, "do not check file existence for packages listed in the internal database of the mirror")
|
||||
cmd.Flag.Bool("latest", false, "download only latest version of each package (per architecture)")
|
||||
cmd.Flag.Int64("download-limit", 0, "limit download speed (kbytes/sec)")
|
||||
cmd.Flag.String("downloader", "default", "downloader to use (e.g. grab)")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
q, err = query.Parse(args[0])
|
||||
value, err := GetStringOrFileContent(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[0], err)
|
||||
}
|
||||
q, err = query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to search: %s", err)
|
||||
}
|
||||
@@ -29,13 +33,14 @@ func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
q = &deb.MatchAllQuery{}
|
||||
}
|
||||
|
||||
result := q.Query(context.CollectionFactory().PackageCollection())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
if result.Len() == 0 {
|
||||
return fmt.Errorf("no results")
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
PrintPackageList(result, format, "")
|
||||
_ = PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -48,6 +53,7 @@ func makeCmdPackageSearch() *commander.Command {
|
||||
Long: `
|
||||
Command search displays list of packages in whole DB that match package query.
|
||||
|
||||
Use '@file' to read query from file or '@-' for stdin.
|
||||
If query is not specified, all the packages are displayed.
|
||||
|
||||
Example:
|
||||
|
||||
+19
-12
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func printReferencesTo(p *deb.Package) (err error) {
|
||||
err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
e := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
func printReferencesTo(p *deb.Package, collectionFactory *deb.CollectionFactory) (err error) {
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -29,8 +29,8 @@ func printReferencesTo(p *deb.Package) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func printReferencesTo(p *deb.Package) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
e := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -66,7 +66,11 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
q, err := query.Parse(args[0])
|
||||
value, err := GetStringOrFileContent(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[0], err)
|
||||
}
|
||||
q, err := query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
@@ -76,11 +80,12 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
w := bufio.NewWriter(os.Stdout)
|
||||
|
||||
result := q.Query(context.CollectionFactory().PackageCollection())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
|
||||
err = result.ForEach(func(p *deb.Package) error {
|
||||
p.Stanza().WriteTo(w, p.IsSource, false)
|
||||
w.Flush()
|
||||
_ = p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
_ = w.Flush()
|
||||
fmt.Printf("\n")
|
||||
|
||||
if withFiles {
|
||||
@@ -104,7 +109,7 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
if withReferences {
|
||||
fmt.Printf("References to package:\n")
|
||||
printReferencesTo(p)
|
||||
_ = printReferencesTo(p, collectionFactory)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
@@ -129,6 +134,8 @@ matching query. Information from Debian control file is displayed.
|
||||
Optionally information about package files and
|
||||
inclusion into mirrors/snapshots/local repos is shown.
|
||||
|
||||
Use '@file' to read query from file or '@-' for stdin.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly package show 'nginx-light_1.2.1-2.2+wheezy2_i386'
|
||||
|
||||
+50
-2
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
@@ -12,7 +14,20 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
}
|
||||
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(flags.Lookup("gpg-key").Value.String())
|
||||
|
||||
var gpgKeys []string
|
||||
|
||||
// CLI args have priority over config
|
||||
cliKeys := flags.Lookup("gpg-key").Value.Get().([]string)
|
||||
if len(cliKeys) > 0 {
|
||||
gpgKeys = cliKeys
|
||||
} else if len(context.Config().GpgKeys) > 0 {
|
||||
gpgKeys = context.Config().GpgKeys
|
||||
}
|
||||
|
||||
for _, gpgKey := range gpgKeys {
|
||||
signer.SetKey(gpgKey)
|
||||
}
|
||||
signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String())
|
||||
signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String())
|
||||
signer.SetBatch(flags.Lookup("batch").Value.Get().(bool))
|
||||
@@ -26,6 +41,23 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
|
||||
}
|
||||
|
||||
type gpgKeyFlag struct {
|
||||
gpgKeys []string
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Set(value string) error {
|
||||
k.gpgKeys = append(k.gpgKeys, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Get() interface{} {
|
||||
return k.gpgKeys
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) String() string {
|
||||
return strings.Join(k.gpgKeys, ",")
|
||||
}
|
||||
|
||||
func makeCmdPublish() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "publish",
|
||||
@@ -34,10 +66,26 @@ func makeCmdPublish() *commander.Command {
|
||||
makeCmdPublishDrop(),
|
||||
makeCmdPublishList(),
|
||||
makeCmdPublishRepo(),
|
||||
makeCmdPublishShow(),
|
||||
makeCmdPublishSnapshot(),
|
||||
makeCmdPublishSource(),
|
||||
makeCmdPublishSwitch(),
|
||||
makeCmdPublishUpdate(),
|
||||
makeCmdPublishShow(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeCmdPublishSource() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "source",
|
||||
Short: "manage sources of published repository",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdPublishSourceAdd(),
|
||||
makeCmdPublishSourceDrop(),
|
||||
makeCmdPublishSourceList(),
|
||||
makeCmdPublishSourceRemove(),
|
||||
makeCmdPublishSourceReplace(),
|
||||
makeCmdPublishSourceUpdate(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -23,8 +23,9 @@ func aptlyPublishDrop(cmd *commander.Command, args []string) error {
|
||||
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Remove(context, storage, prefix, distribution,
|
||||
context.CollectionFactory(), context.Progress(),
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
err = collectionFactory.PublishedRepoCollection().Remove(context, storage, prefix, distribution,
|
||||
collectionFactory, context.Progress(),
|
||||
context.Flags().Lookup("force-drop").Value.Get().(bool),
|
||||
context.Flags().Lookup("skip-cleanup").Value.Get().(bool))
|
||||
if err != nil {
|
||||
|
||||
+58
-5
@@ -1,7 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
@@ -9,19 +11,33 @@ import (
|
||||
)
|
||||
|
||||
func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyPublishListJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyPublishListTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyPublishListTxt(cmd *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
|
||||
published := make([]string, 0, context.CollectionFactory().PublishedRepoCollection().Len())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published := make([]string, 0, collectionFactory.PublishedRepoCollection().Len())
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := context.CollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.CollectionFactory())
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := collectionFactory.PublishedRepoCollection().LoadShallow(repo, collectionFactory)
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error found on one publish (prefix:%s / distribution:%s / component:%s\n)",
|
||||
repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -37,7 +53,7 @@ func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(published)
|
||||
|
||||
@@ -61,6 +77,42 @@ func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyPublishListJSON(_ *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
repos := make([]*deb.PublishedRepo, 0, context.NewCollectionFactory().PublishedRepoCollection().Len())
|
||||
|
||||
err = context.NewCollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error found on one publish (prefix:%s / distribution:%s / component:%s\n)",
|
||||
repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
return e
|
||||
}
|
||||
|
||||
repos = append(repos, repo)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].GetPath() < repos[j].GetPath()
|
||||
})
|
||||
if output, e := json.MarshalIndent(repos, "", " "); e == nil {
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishList,
|
||||
@@ -75,6 +127,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display list in JSON format")
|
||||
cmd.Flag.Bool("raw", false, "display list in machine-readable format")
|
||||
|
||||
return cmd
|
||||
|
||||
+9
-3
@@ -34,20 +34,26 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("origin", "", "origin name to publish")
|
||||
cmd.Flag.String("notautomatic", "", "set value for NotAutomatic field")
|
||||
cmd.Flag.String("butautomaticupgrades", "", "set value for ButAutomaticUpgrades field")
|
||||
cmd.Flag.String("label", "", "label to publish")
|
||||
cmd.Flag.String("suite", "", "suite to publish (defaults to distribution)")
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+51
-5
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,12 +10,23 @@ import (
|
||||
)
|
||||
|
||||
func aptlyPublishShow(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyPublishShowJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyPublishShowTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyPublishShowTxt(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
distribution := args[0]
|
||||
param := "."
|
||||
|
||||
@@ -24,7 +36,8 @@ func aptlyPublishShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
repo, err := context.CollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
@@ -39,16 +52,17 @@ func aptlyPublishShow(cmd *commander.Command, args []string) error {
|
||||
fmt.Printf("Architectures: %s\n", strings.Join(repo.Architectures, " "))
|
||||
|
||||
fmt.Printf("Sources:\n")
|
||||
for component, sourceID := range repo.Sources {
|
||||
for _, component := range repo.Components() {
|
||||
sourceID := repo.Sources[component]
|
||||
var name string
|
||||
if repo.SourceKind == deb.SourceSnapshot {
|
||||
source, e := context.CollectionFactory().SnapshotCollection().ByUUID(sourceID)
|
||||
source, e := collectionFactory.SnapshotCollection().ByUUID(sourceID)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
name = source.Name
|
||||
} else if repo.SourceKind == deb.SourceLocalRepo {
|
||||
source, e := context.CollectionFactory().LocalRepoCollection().ByUUID(sourceID)
|
||||
source, e := collectionFactory.LocalRepoCollection().ByUUID(sourceID)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
@@ -63,6 +77,36 @@ func aptlyPublishShow(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyPublishShowJSON(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
distribution := args[0]
|
||||
param := "."
|
||||
|
||||
if len(args) == 2 {
|
||||
param = args[1]
|
||||
}
|
||||
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
repo, err := context.NewCollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var output []byte
|
||||
if output, err = json.MarshalIndent(repo, "", " "); err == nil {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishShow,
|
||||
@@ -77,5 +121,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+40
-14
@@ -15,6 +15,7 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
if len(args) < len(components) || len(args) > len(components)+1 {
|
||||
cmd.Usage()
|
||||
@@ -43,12 +44,12 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
for _, name := range args {
|
||||
snapshot, err = context.CollectionFactory().SnapshotCollection().ByName(name)
|
||||
snapshot, err = collectionFactory.SnapshotCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
@@ -79,12 +80,12 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
for _, name := range args {
|
||||
localRepo, err = context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
localRepo, err = collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(localRepo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(localRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
@@ -115,8 +116,9 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
origin := context.Flags().Lookup("origin").Value.String()
|
||||
notAutomatic := context.Flags().Lookup("notautomatic").Value.String()
|
||||
butAutomaticUpgrades := context.Flags().Lookup("butautomaticupgrades").Value.String()
|
||||
multiDist := context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, distribution, context.ArchitecturesList(), components, sources, context.CollectionFactory())
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, distribution, context.ArchitecturesList(), components, sources, collectionFactory, multiDist)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
@@ -130,6 +132,8 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
published.ButAutomaticUpgrades = butAutomaticUpgrades
|
||||
}
|
||||
published.Label = context.Flags().Lookup("label").Value.String()
|
||||
published.Suite = context.Flags().Lookup("suite").Value.String()
|
||||
published.Codename = context.Flags().Lookup("codename").Value.String()
|
||||
|
||||
published.SkipContents = context.Config().SkipContentsPublishing
|
||||
|
||||
@@ -137,13 +141,30 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
published.SkipContents = context.Flags().Lookup("skip-contents").Value.Get().(bool)
|
||||
}
|
||||
|
||||
published.SkipBz2 = context.Config().SkipBz2Publishing
|
||||
if context.Flags().IsSet("skip-bz2") {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("acquire-by-hash") {
|
||||
published.AcquireByHash = context.Flags().Lookup("acquire-by-hash").Value.Get().(bool)
|
||||
}
|
||||
|
||||
duplicate := context.CollectionFactory().PublishedRepoCollection().CheckDuplicate(published)
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
duplicate := collectionFactory.PublishedRepoCollection().CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
context.CollectionFactory().PublishedRepoCollection().LoadComplete(duplicate, context.CollectionFactory())
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
@@ -154,16 +175,15 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
|
||||
forceOverwrite := context.Flags().Lookup("force-overwrite").Value.Get().(bool)
|
||||
if forceOverwrite {
|
||||
context.Progress().ColoredPrintf("@rWARNING@|: force overwrite mode enabled, aptly might corrupt other published repositories sharing " +
|
||||
"the same package pool.\n")
|
||||
context.Progress().ColoredPrintf("@rWARNING@|: force overwrite mode enabled, aptly might corrupt other published repositories sharing the same package pool.\n")
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), forceOverwrite)
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Add(published)
|
||||
err = collectionFactory.PublishedRepoCollection().Add(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -218,20 +238,26 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("origin", "", "overwrite origin name to publish")
|
||||
cmd.Flag.String("notautomatic", "", "overwrite value for NotAutomatic field")
|
||||
cmd.Flag.String("butautomaticupgrades", "", "overwrite value for ButAutomaticUpgrades field")
|
||||
cmd.Flag.String("label", "", "label to publish")
|
||||
cmd.Flag.String("suite", "", "suite to publish (defaults to distribution)")
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceAdd(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
distribution := args[0]
|
||||
names := args[1:]
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
if len(names) != len(components) {
|
||||
return fmt.Errorf("mismatch in number of components (%d) and sources (%d)", len(components), len(names))
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
for i, component := range components {
|
||||
name := names[i]
|
||||
_, exists := sources[component]
|
||||
if exists {
|
||||
return fmt.Errorf("unable to add: component '%s' has already been added", component)
|
||||
}
|
||||
context.Progress().Printf("Adding component '%s' with source '%s' [%s]...\n", component, name, published.SourceKind)
|
||||
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nYou can run 'aptly publish update %s %s' to update the content of the published repository.\n",
|
||||
distribution, published.StoragePrefix())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceAdd() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceAdd,
|
||||
UsageLine: "add <distribution> <source>",
|
||||
Short: "add source components to a published repo",
|
||||
Long: `
|
||||
The command adds components of a snapshot or local repository to be published.
|
||||
|
||||
This does not publish the changes directly, but rather schedules them for a subsequent 'aptly publish update'.
|
||||
|
||||
The flag -component is mandatory. Use a comma-separated list of components, if
|
||||
multiple components should be modified. The number of given components must be
|
||||
equal to the number of given sources, e.g.:
|
||||
|
||||
aptly publish source add -component=main,contrib wheezy wheezy-main wheezy-contrib
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source add -component=contrib wheezy ppa wheezy-contrib
|
||||
|
||||
This command assigns the snapshot wheezy-contrib to the component contrib and
|
||||
adds it to published repository revision of ppa/wheezy.
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-add", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceDrop(cmd *commander.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
distribution := args[0]
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
published.DropRevision()
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Source changes have been removed successfully.\n")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceDrop() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceDrop,
|
||||
UsageLine: "drop <distribution>",
|
||||
Short: "drop pending source component changes of a published repository",
|
||||
Long: `
|
||||
Remove all pending changes what would be applied with a subsequent 'aptly publish update'.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source drop wheezy
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-drop", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceList(cmd *commander.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
distribution := args[0]
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
published, err := context.NewCollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(published, context.NewCollectionFactory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if published.Revision == nil {
|
||||
return fmt.Errorf("unable to list: no source changes exist")
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyPublishSourceListJSON(published)
|
||||
}
|
||||
|
||||
return aptlyPublishSourceListTxt(published)
|
||||
}
|
||||
|
||||
func aptlyPublishSourceListTxt(published *deb.PublishedRepo) error {
|
||||
revision := published.Revision
|
||||
|
||||
fmt.Printf("Sources:\n")
|
||||
for _, component := range revision.Components() {
|
||||
name := revision.Sources[component]
|
||||
fmt.Printf(" %s: %s [%s]\n", component, name, published.SourceKind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func aptlyPublishSourceListJSON(published *deb.PublishedRepo) error {
|
||||
revision := published.Revision
|
||||
|
||||
output, err := json.MarshalIndent(revision.SourceList(), "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(output))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceList,
|
||||
UsageLine: "list <distribution>",
|
||||
Short: "lists revision of published repository",
|
||||
Long: `
|
||||
Command lists sources of a published repository.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source list wheezy
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-list", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceRemove(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
distribution := args[0]
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
if len(components) == 0 {
|
||||
return fmt.Errorf("unable to remove: missing components, specify at least one component")
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
for _, component := range components {
|
||||
name, exists := sources[component]
|
||||
if !exists {
|
||||
return fmt.Errorf("unable to remove: component '%s' does not exist", component)
|
||||
}
|
||||
context.Progress().Printf("Removing component '%s' with source '%s' [%s]...\n", component, name, published.SourceKind)
|
||||
|
||||
delete(sources, component)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nYou can run 'aptly publish update %s %s' to update the content of the published repository.\n",
|
||||
distribution, published.StoragePrefix())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceRemove() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceRemove,
|
||||
UsageLine: "remove <distribution> [[<endpoint>:]<prefix>] <source>",
|
||||
Short: "remove source components from a published repo",
|
||||
Long: `
|
||||
The command removes source components (snapshot / local repo) from a published repository.
|
||||
|
||||
This does not publish the changes directly, but rather schedules them for a subsequent 'aptly publish update'.
|
||||
|
||||
The flag -component is mandatory. Use a comma-separated list of components, if
|
||||
multiple components should be removed, e.g.:
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source remove -component=contrib,non-free wheezy filesystem:symlink:debian
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-remove", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to remove (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceReplace(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
distribution := args[0]
|
||||
names := args[1:]
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
if len(names) != len(components) {
|
||||
return fmt.Errorf("mismatch in number of components (%d) and sources (%d)", len(components), len(names))
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
context.Progress().Printf("Replacing source list...\n")
|
||||
clear(sources)
|
||||
|
||||
for i, component := range components {
|
||||
name := names[i]
|
||||
context.Progress().Printf("Adding component '%s' with source '%s' [%s]...\n", component, name, published.SourceKind)
|
||||
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nYou can run 'aptly publish update %s %s' to update the content of the published repository.\n",
|
||||
distribution, published.StoragePrefix())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceReplace() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceReplace,
|
||||
UsageLine: "replace <distribution> <source>",
|
||||
Short: "replace the source components of a published repository",
|
||||
Long: `
|
||||
The command replaces the source components of a snapshot or local repository to be published.
|
||||
|
||||
This does not publish the changes directly, but rather schedules them for a subsequent 'aptly publish update'.
|
||||
|
||||
The flag -component is mandatory. Use a comma-separated list of components, if
|
||||
multiple components should be modified. The number of given components must be
|
||||
equal to the number of given sources, e.g.:
|
||||
|
||||
aptly publish source replace -component=main,contrib wheezy wheezy-main wheezy-contrib
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source replace -component=contrib wheezy ppa wheezy-contrib
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-add", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceUpdate(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
distribution := args[0]
|
||||
names := args[1:]
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
if len(names) != len(components) {
|
||||
return fmt.Errorf("mismatch in number of components (%d) and sources (%d)", len(components), len(names))
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
for i, component := range components {
|
||||
name := names[i]
|
||||
_, exists := sources[component]
|
||||
if !exists {
|
||||
return fmt.Errorf("unable to update: component '%s' does not exist", component)
|
||||
}
|
||||
context.Progress().Printf("Updating component '%s' with source '%s' [%s]...\n", component, name, published.SourceKind)
|
||||
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nYou can run 'aptly publish update %s %s' to update the content of the published repository.\n",
|
||||
distribution, published.StoragePrefix())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceUpdate() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceUpdate,
|
||||
UsageLine: "update <distribution> <source>",
|
||||
Short: "update the source components of a published repository",
|
||||
Long: `
|
||||
The command updates the source components of a snapshot or local repository to be published.
|
||||
|
||||
This does not publish the changes directly, but rather schedules them for a subsequent 'aptly publish update'.
|
||||
|
||||
The flag -component is mandatory. Use a comma-separated list of components, if
|
||||
multiple components should be modified. The number of given components must be
|
||||
equal to the number of given sources, e.g.:
|
||||
|
||||
aptly publish source update -component=main,contrib wheezy wheezy-main wheezy-contrib
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source update -component=contrib wheezy ppa wheezy-contrib
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-update", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
+46
-27
@@ -11,7 +11,10 @@ import (
|
||||
)
|
||||
|
||||
func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
var (
|
||||
err error
|
||||
names []string
|
||||
)
|
||||
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
@@ -23,11 +26,6 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
distribution := args[0]
|
||||
param := "."
|
||||
|
||||
var (
|
||||
names []string
|
||||
snapshot *deb.Snapshot
|
||||
)
|
||||
|
||||
if len(args) == len(components)+2 {
|
||||
param = args[1]
|
||||
names = args[2:]
|
||||
@@ -39,18 +37,19 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
|
||||
var published *deb.PublishedRepo
|
||||
|
||||
published, err = context.CollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err = collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
return fmt.Errorf("unable to switch: %s", err)
|
||||
}
|
||||
|
||||
if published.SourceKind != deb.SourceSnapshot {
|
||||
return fmt.Errorf("unable to update: not a snapshot publish")
|
||||
return fmt.Errorf("unable to switch: not a published snapshot repository")
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().LoadComplete(published, context.CollectionFactory())
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
return fmt.Errorf("unable to switch: %s", err)
|
||||
}
|
||||
|
||||
publishedComponents := published.Components()
|
||||
@@ -62,17 +61,18 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("mismatch in number of components (%d) and snapshots (%d)", len(components), len(names))
|
||||
}
|
||||
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
for i, component := range components {
|
||||
if !utils.StrSliceHasItem(publishedComponents, component) {
|
||||
return fmt.Errorf("unable to switch: component %s is not in published repository", component)
|
||||
return fmt.Errorf("unable to switch: component %s does not exist in published repository", component)
|
||||
}
|
||||
|
||||
snapshot, err = context.CollectionFactory().SnapshotCollection().ByName(names[i])
|
||||
snapshot, err := snapshotCollection.ByName(names[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to switch: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
err = snapshotCollection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to switch: %s", err)
|
||||
}
|
||||
@@ -95,26 +95,41 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
published.SkipContents = context.Flags().Lookup("skip-contents").Value.Get().(bool)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), forceOverwrite)
|
||||
if context.Flags().IsSet("skip-bz2") {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Update(published)
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
skipCleanup := context.Flags().Lookup("skip-cleanup").Value.Get().(bool)
|
||||
if !skipCleanup {
|
||||
err = context.CollectionFactory().PublishedRepoCollection().CleanupPrefixComponentFiles(published.Prefix, components,
|
||||
context.GetPublishedStorage(storage), context.CollectionFactory(), context.Progress())
|
||||
err = collectionFactory.PublishedRepoCollection().CleanupPrefixComponentFiles(context, published, components, collectionFactory, context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
return fmt.Errorf("unable to switch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nPublish for snapshot %s has been successfully switched to new snapshot.\n", published.String())
|
||||
context.Progress().Printf("\nPublished %s repository %s has been successfully switched to new source.\n", published.SourceKind, published.String())
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -122,15 +137,15 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
func makeCmdPublishSwitch() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSwitch,
|
||||
UsageLine: "switch <distribution> [[<endpoint>:]<prefix>] <new-snapshot>",
|
||||
Short: "update published repository by switching to new snapshot",
|
||||
UsageLine: "switch <distribution> [[<endpoint>:]<prefix>] <new-source>",
|
||||
Short: "update published repository by switching to new source",
|
||||
Long: `
|
||||
Command switches in-place published snapshots with new snapshot contents. All
|
||||
Command switches in-place published snapshots with new source contents. All
|
||||
publishing parameters are preserved (architecture list, distribution,
|
||||
component).
|
||||
|
||||
For multiple component repositories, flag -component should be given with
|
||||
list of components to update. Corresponding snapshots should be given in the
|
||||
list of components to update. Corresponding sources should be given in the
|
||||
same order, e.g.:
|
||||
|
||||
aptly publish switch -component=main,contrib wheezy wh-main wh-contrib
|
||||
@@ -144,17 +159,21 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-switch", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("component", "", "component names to update (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+58
-24
@@ -25,23 +25,20 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error {
|
||||
|
||||
var published *deb.PublishedRepo
|
||||
|
||||
published, err = context.CollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err = collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if published.SourceKind != deb.SourceLocalRepo {
|
||||
return fmt.Errorf("unable to update: not a local repository publish")
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().LoadComplete(published, context.CollectionFactory())
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
components := published.Components()
|
||||
for _, component := range components {
|
||||
published.UpdateLocalRepo(component)
|
||||
result, err := published.Update(collectionFactory, context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
signer, err := getSigner(context.Flags())
|
||||
@@ -59,26 +56,51 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error {
|
||||
published.SkipContents = context.Flags().Lookup("skip-contents").Value.Get().(bool)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), forceOverwrite)
|
||||
if context.Flags().IsSet("skip-bz2") {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("origin") {
|
||||
published.Origin = context.Flags().Lookup("origin").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("label") {
|
||||
published.Label = context.Flags().Lookup("label").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Update(published)
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
skipCleanup := context.Flags().Lookup("skip-cleanup").Value.Get().(bool)
|
||||
if !skipCleanup {
|
||||
err = context.CollectionFactory().PublishedRepoCollection().CleanupPrefixComponentFiles(published.Prefix, components,
|
||||
context.GetPublishedStorage(storage), context.CollectionFactory(), context.Progress())
|
||||
cleanComponents := make([]string, 0, len(result.UpdatedSources)+len(result.RemovedSources))
|
||||
cleanComponents = append(append(cleanComponents, result.UpdatedComponents()...), result.RemovedComponents()...)
|
||||
err = collectionFactory.PublishedRepoCollection().CleanupPrefixComponentFiles(context, published, cleanComponents, collectionFactory, context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nPublish for local repo %s has been successfully updated.\n", published.String())
|
||||
context.Progress().Printf("\nPublished %s repository %s has been updated successfully.\n", published.SourceKind, published.String())
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -87,15 +109,21 @@ func makeCmdPublishUpdate() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishUpdate,
|
||||
UsageLine: "update <distribution> [[<endpoint>:]<prefix>]",
|
||||
Short: "update published local repository",
|
||||
Short: "update published repository",
|
||||
Long: `
|
||||
Command re-publishes (updates) published local repository. <distribution>
|
||||
and <prefix> should be occupied with local repository published
|
||||
using command aptly publish repo. Update happens in-place with
|
||||
minimum possible downtime for published repository.
|
||||
The command updates updates a published repository after applying pending changes to the sources.
|
||||
|
||||
For multiple component published repositories, all local repositories
|
||||
are updated.
|
||||
For published local repositories:
|
||||
|
||||
* update to match local repository contents
|
||||
|
||||
For published snapshots:
|
||||
|
||||
* switch components to new snapshot
|
||||
|
||||
The update happens in-place with minimum possible downtime for published repository.
|
||||
|
||||
For multiple component published repositories, all local repositories are updated.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -103,16 +131,22 @@ Example:
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-update", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("origin", "", "overwrite origin name to publish")
|
||||
cmd.Flag.String("label", "", "overwrite label to publish")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+8
-7
@@ -22,19 +22,20 @@ func aptlyRepoAdd(cmd *commander.Command, args []string) error {
|
||||
|
||||
verifier := context.GetVerifier()
|
||||
|
||||
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Loading packages...\n")
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
@@ -48,8 +49,8 @@ func aptlyRepoAdd(cmd *commander.Command, args []string) error {
|
||||
var processedFiles, failedFiles2 []string
|
||||
|
||||
processedFiles, failedFiles2, err = deb.ImportPackageFiles(list, packageFiles, forceReplace, verifier, context.PackagePool(),
|
||||
context.CollectionFactory().PackageCollection(), &aptly.ConsoleResultReporter{Progress: context.Progress()}, nil,
|
||||
context.CollectionFactory().ChecksumCollection())
|
||||
collectionFactory.PackageCollection(), &aptly.ConsoleResultReporter{Progress: context.Progress()}, nil,
|
||||
collectionFactory.ChecksumCollection)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import package files: %s", err)
|
||||
@@ -59,7 +60,7 @@ func aptlyRepoAdd(cmd *commander.Command, args []string) error {
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -90,7 +91,7 @@ func aptlyRepoAdd(cmd *commander.Command, args []string) error {
|
||||
func makeCmdRepoAdd() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyRepoAdd,
|
||||
UsageLine: "add <name> <package file.deb>|<directory> ...",
|
||||
UsageLine: "add <name> (<package file.deb>|<directory>)...",
|
||||
Short: "add packages to local repository",
|
||||
Long: `
|
||||
Command adds packages to local repository from .deb, .udeb (binary packages) and .dsc (source packages) files.
|
||||
|
||||
+4
-3
@@ -27,15 +27,16 @@ func aptlyRepoCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
if len(args) == 4 {
|
||||
var snapshot *deb.Snapshot
|
||||
|
||||
snapshot, err = context.CollectionFactory().SnapshotCollection().ByName(args[3])
|
||||
snapshot, err = collectionFactory.SnapshotCollection().ByName(args[3])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load source snapshot: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load source snapshot: %s", err)
|
||||
}
|
||||
@@ -43,7 +44,7 @@ func aptlyRepoCreate(cmd *commander.Command, args []string) error {
|
||||
repo.UpdateRefList(snapshot.RefList())
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Add(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Add(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add local repo: %s", err)
|
||||
}
|
||||
|
||||
+6
-5
@@ -15,17 +15,18 @@ func aptlyRepoDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
published := context.CollectionFactory().PublishedRepoCollection().ByLocalRepo(repo)
|
||||
published := collectionFactory.PublishedRepoCollection().ByLocalRepo(repo)
|
||||
if len(published) > 0 {
|
||||
fmt.Printf("Local repo `%s` is published currently:\n", repo.Name)
|
||||
for _, repo := range published {
|
||||
err = context.CollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.CollectionFactory())
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(repo, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load published: %s", err)
|
||||
}
|
||||
@@ -37,7 +38,7 @@ func aptlyRepoDrop(cmd *commander.Command, args []string) error {
|
||||
|
||||
force := context.Flags().Lookup("force").Value.Get().(bool)
|
||||
if !force {
|
||||
snapshots := context.CollectionFactory().SnapshotCollection().ByLocalRepoSource(repo)
|
||||
snapshots := collectionFactory.SnapshotCollection().ByLocalRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
fmt.Printf("Local repo `%s` was used to create following snapshots:\n", repo.Name)
|
||||
@@ -49,7 +50,7 @@ func aptlyRepoDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Drop(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Drop(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
+4
-3
@@ -16,12 +16,13 @@ func aptlyRepoEdit(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(args[0])
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func aptlyRepoEdit(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
|
||||
+17
-143
@@ -1,16 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
@@ -33,10 +29,16 @@ func aptlyRepoInclude(cmd *commander.Command, args []string) error {
|
||||
|
||||
forceReplace := context.Flags().Lookup("force-replace").Value.Get().(bool)
|
||||
acceptUnsigned := context.Flags().Lookup("accept-unsigned").Value.Get().(bool)
|
||||
ignoreSignatures := context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
noRemoveFiles := context.Flags().Lookup("no-remove-files").Value.Get().(bool)
|
||||
repoTemplateString := context.Flags().Lookup("repo").Value.Get().(string)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repoTemplate, err := template.New("repo").Parse(context.Flags().Lookup("repo").Value.Get().(string))
|
||||
var repoTemplate *template.Template
|
||||
repoTemplate, err = template.New("repo").Parse(repoTemplateString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing -repo template: %s", err)
|
||||
}
|
||||
@@ -59,143 +61,15 @@ func aptlyRepoInclude(cmd *commander.Command, args []string) error {
|
||||
|
||||
reporter := &aptly.ConsoleResultReporter{Progress: context.Progress()}
|
||||
|
||||
var changesFiles, failedFiles, processedFiles []string
|
||||
var changesFiles, failedFiles, failedFiles2 []string
|
||||
|
||||
changesFiles, failedFiles = deb.CollectChangesFiles(args, reporter)
|
||||
|
||||
for _, path := range changesFiles {
|
||||
var changes *deb.Changes
|
||||
|
||||
changes, err = deb.NewChanges(path)
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", path, err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = changes.VerifyAndParse(acceptUnsigned, ignoreSignatures, verifier)
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
err = changes.Prepare()
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
repoName := &bytes.Buffer{}
|
||||
err = repoTemplate.Execute(repoName, changes.Stanza)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error applying template to repo: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Loading repository %s for changes file %s...\n", repoName.String(), changes.ChangesName)
|
||||
|
||||
var repo *deb.LocalRepo
|
||||
repo, err = context.CollectionFactory().LocalRepoCollection().ByName(repoName.String())
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
currentUploaders := uploaders
|
||||
if repo.Uploaders != nil {
|
||||
currentUploaders = repo.Uploaders
|
||||
for i := range currentUploaders.Rules {
|
||||
currentUploaders.Rules[i].CompiledCondition, err = query.Parse(currentUploaders.Rules[i].Condition)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing query %s: %s", currentUploaders.Rules[i].Condition, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentUploaders != nil {
|
||||
if err = currentUploaders.IsAllowed(changes); err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("changes file skipped due to uploaders config: %s, keys %#v: %s",
|
||||
changes.ChangesName, changes.SignatureKeys, err)
|
||||
changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load repo: %s", err)
|
||||
}
|
||||
|
||||
var list *deb.PackageList
|
||||
list, err = deb.NewPackageListFromRefList(repo.RefList(), context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
packageFiles, otherFiles, _ := deb.CollectPackageFiles([]string{changes.TempDir}, reporter)
|
||||
|
||||
var restriction deb.PackageQuery
|
||||
|
||||
restriction, err = changes.PackageQuery()
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
var processedFiles2, failedFiles2 []string
|
||||
|
||||
processedFiles2, failedFiles2, err = deb.ImportPackageFiles(list, packageFiles, forceReplace, verifier, context.PackagePool(),
|
||||
context.CollectionFactory().PackageCollection(), reporter, restriction, context.CollectionFactory().ChecksumCollection())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import package files: %s", err)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
|
||||
err = changes.Cleanup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range failedFiles2 {
|
||||
failedFiles = append(failedFiles, filepath.Join(changes.BasePath, filepath.Base(file)))
|
||||
}
|
||||
|
||||
for _, file := range processedFiles2 {
|
||||
processedFiles = append(processedFiles, filepath.Join(changes.BasePath, filepath.Base(file)))
|
||||
}
|
||||
|
||||
for _, file := range otherFiles {
|
||||
processedFiles = append(processedFiles, filepath.Join(changes.BasePath, filepath.Base(file)))
|
||||
}
|
||||
|
||||
processedFiles = append(processedFiles, path)
|
||||
}
|
||||
|
||||
if !noRemoveFiles {
|
||||
processedFiles = utils.StrSliceDeduplicate(processedFiles)
|
||||
|
||||
for _, file := range processedFiles {
|
||||
err = os.Remove(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove file: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, failedFiles2, err = deb.ImportChangesFiles(
|
||||
changesFiles, reporter, acceptUnsigned, ignoreSignatures, forceReplace, noRemoveFiles, verifier, repoTemplate,
|
||||
context.Progress(), collectionFactory.LocalRepoCollection(), collectionFactory.PackageCollection(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection,
|
||||
uploaders, query.Parse)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
|
||||
if len(failedFiles) > 0 {
|
||||
context.Progress().ColoredPrintf("@y[!]@| @!Some files were skipped due to errors:@|")
|
||||
@@ -212,14 +86,14 @@ func aptlyRepoInclude(cmd *commander.Command, args []string) error {
|
||||
func makeCmdRepoInclude() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyRepoInclude,
|
||||
UsageLine: "include <file.changes>|<directory> ...",
|
||||
UsageLine: "include (<file.changes>|<directory>)...",
|
||||
Short: "add packages to local repositories based on .changes files",
|
||||
Long: `
|
||||
Command include looks for .changes files in list of arguments or specified directories. Each
|
||||
.changes file is verified, parsed, referenced files are put into separate temporary directory
|
||||
and added into local repository. Successfully imported files are removed by default.
|
||||
|
||||
Additionally uploads could be restricted with <uploaders.json> file. Rules in this file control
|
||||
Additionally uploads could be restricted with 'uploaders.json' file. Rules in this file control
|
||||
uploads based on GPG key ID of .changes file signature and queries on .changes file fields.
|
||||
|
||||
Example:
|
||||
|
||||
+49
-5
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
@@ -9,21 +10,33 @@ import (
|
||||
)
|
||||
|
||||
func aptlyRepoList(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyRepoListJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyRepoListTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyRepoListTxt(cmd *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
|
||||
repos := make([]string, context.CollectionFactory().LocalRepoCollection().Len())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := make([]string, collectionFactory.LocalRepoCollection().Len())
|
||||
i := 0
|
||||
context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
_ = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
e := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -34,7 +47,7 @@ func aptlyRepoList(cmd *commander.Command, args []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -58,6 +71,36 @@ func aptlyRepoList(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyRepoListJSON(_ *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
repos := make([]*deb.LocalRepo, context.NewCollectionFactory().LocalRepoCollection().Len())
|
||||
i := 0
|
||||
_ = context.NewCollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := context.NewCollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
repos[i] = repo
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
})
|
||||
if output, e := json.MarshalIndent(repos, "", " "); e == nil {
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdRepoList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyRepoList,
|
||||
@@ -72,6 +115,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display list in JSON format")
|
||||
cmd.Flag.Bool("raw", false, "display list in machine-readable format")
|
||||
|
||||
return cmd
|
||||
|
||||
+26
-12
@@ -19,12 +19,13 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
|
||||
command := cmd.Name()
|
||||
|
||||
dstRepo, err := context.CollectionFactory().LocalRepoCollection().ByName(args[1])
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
dstRepo, err := collectionFactory.LocalRepoCollection().ByName(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(dstRepo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(dstRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
@@ -35,7 +36,7 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
if command == "copy" || command == "move" { // nolint: goconst
|
||||
srcRepo, err = context.CollectionFactory().LocalRepoCollection().ByName(args[0])
|
||||
srcRepo, err = collectionFactory.LocalRepoCollection().ByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to %s: source and destination are the same", command)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(srcRepo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(srcRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
@@ -53,12 +54,12 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
} else if command == "import" { // nolint: goconst
|
||||
var srcRemoteRepo *deb.RemoteRepo
|
||||
|
||||
srcRemoteRepo, err = context.CollectionFactory().RemoteRepoCollection().ByName(args[0])
|
||||
srcRemoteRepo, err = collectionFactory.RemoteRepoCollection().ByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(srcRemoteRepo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(srcRemoteRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
@@ -74,12 +75,12 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
|
||||
context.Progress().Printf("Loading packages...\n")
|
||||
|
||||
dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
srcList, err := deb.NewPackageListFromRefList(srcRefList, context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
srcList, err := deb.NewPackageListFromRefList(srcRefList, collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
@@ -109,13 +110,24 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
|
||||
queries := make([]deb.PackageQuery, len(args)-2)
|
||||
for i := 0; i < len(args)-2; i++ {
|
||||
queries[i], err = query.Parse(args[i+2])
|
||||
value, err := GetStringOrFileContent(args[i+2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[i+2], err)
|
||||
}
|
||||
queries[i], err = query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
}
|
||||
|
||||
toProcess, err := srcList.FilterWithProgress(queries, withDeps, dstList, context.DependencyOptions(), architecturesList, context.Progress())
|
||||
toProcess, err := srcList.Filter(deb.FilterOptions{
|
||||
Queries: queries,
|
||||
WithDependencies: withDeps,
|
||||
Source: dstList,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
Progress: context.Progress(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to %s: %s", command, err)
|
||||
}
|
||||
@@ -151,7 +163,7 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
} else {
|
||||
dstRepo.UpdateRefList(deb.NewPackageRefListFromPackageList(dstList))
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(dstRepo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(dstRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -159,7 +171,7 @@ func aptlyRepoMoveCopyImport(cmd *commander.Command, args []string) error {
|
||||
if command == "move" { // nolint: goconst
|
||||
srcRepo.UpdateRefList(deb.NewPackageRefListFromPackageList(srcList))
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(srcRepo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(srcRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -178,6 +190,8 @@ func makeCmdRepoMove() *commander.Command {
|
||||
Command move moves packages matching <package-query> from local repo
|
||||
<src-name> to local repo <dst-name>.
|
||||
|
||||
Use '@file' to read package queries from file or '@-' for stdin.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly repo move testing stable 'myapp (=0.1.12)'
|
||||
|
||||
+14
-7
@@ -18,38 +18,43 @@ func aptlyRepoRemove(cmd *commander.Command, args []string) error {
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Loading packages...\n")
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), context.CollectionFactory().PackageCollection(), context.Progress())
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
queries := make([]deb.PackageQuery, len(args)-1)
|
||||
for i := 0; i < len(args)-1; i++ {
|
||||
queries[i], err = query.Parse(args[i+1])
|
||||
value, err := GetStringOrFileContent(args[i+1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[i+1], err)
|
||||
}
|
||||
queries[i], err = query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
toRemove, err := list.Filter(queries, false, nil, 0, nil)
|
||||
toRemove, err := list.Filter(deb.FilterOptions{Queries: queries})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
toRemove.ForEach(func(p *deb.Package) error {
|
||||
_ = toRemove.ForEach(func(p *deb.Package) error {
|
||||
list.Remove(p)
|
||||
context.Progress().ColoredPrintf("@r[-]@| %s removed", p)
|
||||
return nil
|
||||
@@ -60,7 +65,7 @@ func aptlyRepoRemove(cmd *commander.Command, args []string) error {
|
||||
} else {
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -80,6 +85,8 @@ Commands removes packages matching <package-query> from local repository
|
||||
snapshots, they can be removed completely (including files) by running
|
||||
'aptly db cleanup'.
|
||||
|
||||
Use '@file' to read package queries from file or '@-' for stdin.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly repo remove testing 'myapp (=0.1.12)'
|
||||
|
||||
+4
-3
@@ -20,18 +20,19 @@ func aptlyRepoRename(cmd *commander.Command, args []string) error {
|
||||
|
||||
oldName, newName := args[0], args[1]
|
||||
|
||||
repo, err = context.CollectionFactory().LocalRepoCollection().ByName(oldName)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err = collectionFactory.LocalRepoCollection().ByName(oldName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
_, err = context.CollectionFactory().LocalRepoCollection().ByName(newName)
|
||||
_, err = collectionFactory.LocalRepoCollection().ByName(newName)
|
||||
if err == nil {
|
||||
return fmt.Errorf("unable to rename: local repo %s already exists", newName)
|
||||
}
|
||||
|
||||
repo.Name = newName
|
||||
err = context.CollectionFactory().LocalRepoCollection().Update(repo)
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
+62
-4
@@ -1,27 +1,42 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyRepoShow(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyRepoShowJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyRepoShowTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyRepoShowTxt(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
@@ -37,7 +52,49 @@ func aptlyRepoShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
ListPackagesRefList(repo.RefList())
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyRepoShowJSON(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.NewCollectionFactory().LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
// include packages if requested
|
||||
packageList := []string{}
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
if repo.RefList() != nil {
|
||||
var list *deb.PackageList
|
||||
list, err = deb.NewPackageListFromRefList(repo.RefList(), context.NewCollectionFactory().PackageCollection(), context.Progress())
|
||||
if err == nil {
|
||||
packageList = list.FullNames()
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(packageList)
|
||||
}
|
||||
|
||||
// merge the repo object with the package list
|
||||
var output []byte
|
||||
if output, err = json.MarshalIndent(struct {
|
||||
*deb.LocalRepo
|
||||
Packages []string
|
||||
}{repo, packageList}, "", " "); err == nil {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -57,6 +114,7 @@ ex:
|
||||
Flag: *flag.NewFlagSet("aptly-repo-show", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
cmd.Flag.Bool("with-packages", false, "show list of packages")
|
||||
|
||||
return cmd
|
||||
|
||||
+7
-6
@@ -29,12 +29,13 @@ func aptlyServe(cmd *commander.Command, args []string) error {
|
||||
// anything else must fail.
|
||||
// E.g.: Running the service under a different user may lead to a rootDir
|
||||
// that exists but is not usable due to access permissions.
|
||||
err = utils.DirIsAccessible(context.Config().RootDir)
|
||||
err = utils.DirIsAccessible(context.Config().GetRootDir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if context.CollectionFactory().PublishedRepoCollection().Len() == 0 {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
if collectionFactory.PublishedRepoCollection().Len() == 0 {
|
||||
fmt.Printf("No published repositories, unable to serve.\n")
|
||||
return nil
|
||||
}
|
||||
@@ -56,11 +57,11 @@ func aptlyServe(cmd *commander.Command, args []string) error {
|
||||
|
||||
fmt.Printf("Serving published repositories, recommended apt sources list:\n\n")
|
||||
|
||||
sources := make(sort.StringSlice, 0, context.CollectionFactory().PublishedRepoCollection().Len())
|
||||
published := make(map[string]*deb.PublishedRepo, context.CollectionFactory().PublishedRepoCollection().Len())
|
||||
sources := make(sort.StringSlice, 0, collectionFactory.PublishedRepoCollection().Len())
|
||||
published := make(map[string]*deb.PublishedRepo, collectionFactory.PublishedRepoCollection().Len())
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := context.CollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.CollectionFactory())
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := collectionFactory.PublishedRepoCollection().LoadComplete(repo, collectionFactory)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ func aptlySnapshotCreate(cmd *commander.Command, args []string) error {
|
||||
snapshot *deb.Snapshot
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
if len(args) == 4 && args[1] == "from" && args[2] == "mirror" { // nolint: goconst
|
||||
// aptly snapshot create snap from mirror mirror
|
||||
var repo *deb.RemoteRepo
|
||||
|
||||
repoName, snapshotName := args[3], args[0]
|
||||
|
||||
repo, err = context.CollectionFactory().RemoteRepoCollection().ByName(repoName)
|
||||
repo, err = collectionFactory.RemoteRepoCollection().ByName(repoName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
@@ -29,7 +30,7 @@ func aptlySnapshotCreate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
@@ -44,12 +45,12 @@ func aptlySnapshotCreate(cmd *commander.Command, args []string) error {
|
||||
|
||||
localRepoName, snapshotName := args[3], args[0]
|
||||
|
||||
repo, err = context.CollectionFactory().LocalRepoCollection().ByName(localRepoName)
|
||||
repo, err = collectionFactory.LocalRepoCollection().ByName(localRepoName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func aptlySnapshotCreate(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().Add(snapshot)
|
||||
err = collectionFactory.SnapshotCollection().Add(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add snapshot: %s", err)
|
||||
}
|
||||
@@ -83,7 +84,7 @@ func aptlySnapshotCreate(cmd *commander.Command, args []string) error {
|
||||
func makeCmdSnapshotCreate() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlySnapshotCreate,
|
||||
UsageLine: "create <name> from mirror <mirror-name> | from repo <repo-name> | empty",
|
||||
UsageLine: "create <name> (from mirror <mirror-name> | from repo <repo-name> | empty)",
|
||||
Short: "creates snapshot of mirror (local repository) contents",
|
||||
Long: `
|
||||
Command create <name> from mirror makes persistent immutable snapshot of remote
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user