Commit

Author:

Hash:

Timestamp:

+1658 -193 +/-14 browse

Kevin Schoon [me@kevinschoon.com]

b472ebf804e354d9f81899340a43676d42027f67

Sat, 29 Mar 2025 20:40:59 +0000 (4 months ago)

hack together more basic implementation
1diff --git a/.gitignore b/.gitignore
2index ea8c4bf..57ac264 100644
3--- a/.gitignore
4+++ b/.gitignore
5 @@ -1 +1,2 @@
6 /target
7+ registry
8 diff --git a/Cargo.lock b/Cargo.lock
9index 8f700ae..a1fce97 100644
10--- a/Cargo.lock
11+++ b/Cargo.lock
12 @@ -38,12 +38,19 @@ dependencies = [
13 ]
14
15 [[package]]
16+ name = "autocfg"
17+ version = "1.4.0"
18+ source = "registry+https://github.com/rust-lang/crates.io-index"
19+ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
20+
21+ [[package]]
22 name = "axum"
23- version = "0.8.1"
24+ version = "0.8.3"
25 source = "registry+https://github.com/rust-lang/crates.io-index"
26- checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
27+ checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
28 dependencies = [
29 "axum-core",
30+ "axum-macros",
31 "bytes",
32 "form_urlencoded",
33 "futures-util",
34 @@ -73,12 +80,12 @@ dependencies = [
35
36 [[package]]
37 name = "axum-core"
38- version = "0.5.0"
39+ version = "0.5.2"
40 source = "registry+https://github.com/rust-lang/crates.io-index"
41- checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
42+ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
43 dependencies = [
44 "bytes",
45- "futures-util",
46+ "futures-core",
47 "http",
48 "http-body",
49 "http-body-util",
50 @@ -92,6 +99,17 @@ dependencies = [
51 ]
52
53 [[package]]
54+ name = "axum-macros"
55+ version = "0.5.0"
56+ source = "registry+https://github.com/rust-lang/crates.io-index"
57+ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
58+ dependencies = [
59+ "proc-macro2",
60+ "quote",
61+ "syn",
62+ ]
63+
64+ [[package]]
65 name = "backtrace"
66 version = "0.3.74"
67 source = "registry+https://github.com/rust-lang/crates.io-index"
68 @@ -107,6 +125,12 @@ dependencies = [
69 ]
70
71 [[package]]
72+ name = "bitflags"
73+ version = "2.9.0"
74+ source = "registry+https://github.com/rust-lang/crates.io-index"
75+ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
76+
77+ [[package]]
78 name = "bytes"
79 version = "1.10.1"
80 source = "registry+https://github.com/rust-lang/crates.io-index"
81 @@ -200,12 +224,28 @@ dependencies = [
82 ]
83
84 [[package]]
85+ name = "futures"
86+ version = "0.3.31"
87+ source = "registry+https://github.com/rust-lang/crates.io-index"
88+ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
89+ dependencies = [
90+ "futures-channel",
91+ "futures-core",
92+ "futures-executor",
93+ "futures-io",
94+ "futures-sink",
95+ "futures-task",
96+ "futures-util",
97+ ]
98+
99+ [[package]]
100 name = "futures-channel"
101 version = "0.3.31"
102 source = "registry+https://github.com/rust-lang/crates.io-index"
103 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
104 dependencies = [
105 "futures-core",
106+ "futures-sink",
107 ]
108
109 [[package]]
110 @@ -215,6 +255,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
111 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
112
113 [[package]]
114+ name = "futures-executor"
115+ version = "0.3.31"
116+ source = "registry+https://github.com/rust-lang/crates.io-index"
117+ checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
118+ dependencies = [
119+ "futures-core",
120+ "futures-task",
121+ "futures-util",
122+ ]
123+
124+ [[package]]
125+ name = "futures-io"
126+ version = "0.3.31"
127+ source = "registry+https://github.com/rust-lang/crates.io-index"
128+ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
129+
130+ [[package]]
131+ name = "futures-macro"
132+ version = "0.3.31"
133+ source = "registry+https://github.com/rust-lang/crates.io-index"
134+ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
135+ dependencies = [
136+ "proc-macro2",
137+ "quote",
138+ "syn",
139+ ]
140+
141+ [[package]]
142+ name = "futures-sink"
143+ version = "0.3.31"
144+ source = "registry+https://github.com/rust-lang/crates.io-index"
145+ checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
146+
147+ [[package]]
148 name = "futures-task"
149 version = "0.3.31"
150 source = "registry+https://github.com/rust-lang/crates.io-index"
151 @@ -226,10 +300,28 @@ version = "0.3.31"
152 source = "registry+https://github.com/rust-lang/crates.io-index"
153 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
154 dependencies = [
155+ "futures-channel",
156 "futures-core",
157+ "futures-io",
158+ "futures-macro",
159+ "futures-sink",
160 "futures-task",
161+ "memchr",
162 "pin-project-lite",
163 "pin-utils",
164+ "slab",
165+ ]
166+
167+ [[package]]
168+ name = "getrandom"
169+ version = "0.3.1"
170+ source = "registry+https://github.com/rust-lang/crates.io-index"
171+ checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
172+ dependencies = [
173+ "cfg-if",
174+ "libc",
175+ "wasi 0.13.3+wasi-0.2.2",
176+ "windows-targets",
177 ]
178
179 [[package]]
180 @@ -350,12 +442,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
181 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
182
183 [[package]]
184+ name = "lazy_static"
185+ version = "1.5.0"
186+ source = "registry+https://github.com/rust-lang/crates.io-index"
187+ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
188+
189+ [[package]]
190 name = "libc"
191 version = "0.2.171"
192 source = "registry+https://github.com/rust-lang/crates.io-index"
193 checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
194
195 [[package]]
196+ name = "lock_api"
197+ version = "0.4.12"
198+ source = "registry+https://github.com/rust-lang/crates.io-index"
199+ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
200+ dependencies = [
201+ "autocfg",
202+ "scopeguard",
203+ ]
204+
205+ [[package]]
206 name = "log"
207 version = "0.4.26"
208 source = "registry+https://github.com/rust-lang/crates.io-index"
209 @@ -395,11 +503,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
210 checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
211 dependencies = [
212 "libc",
213- "wasi",
214+ "wasi 0.11.0+wasi-snapshot-preview1",
215 "windows-sys",
216 ]
217
218 [[package]]
219+ name = "nu-ansi-term"
220+ version = "0.46.0"
221+ source = "registry+https://github.com/rust-lang/crates.io-index"
222+ checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
223+ dependencies = [
224+ "overload",
225+ "winapi",
226+ ]
227+
228+ [[package]]
229 name = "object"
230 version = "0.36.7"
231 source = "registry+https://github.com/rust-lang/crates.io-index"
232 @@ -431,17 +549,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
233 checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
234
235 [[package]]
236+ name = "overload"
237+ version = "0.1.1"
238+ source = "registry+https://github.com/rust-lang/crates.io-index"
239+ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
240+
241+ [[package]]
242 name = "papyri"
243 version = "0.1.0"
244 dependencies = [
245 "async-trait",
246 "axum",
247+ "bytes",
248+ "futures",
249+ "http",
250 "oci-spec",
251 "regex",
252 "serde",
253 "serde_json",
254 "thiserror",
255+ "tokio",
256+ "tower",
257+ "tower-http",
258 "tracing",
259+ "tracing-subscriber",
260+ "uuid",
261+ ]
262+
263+ [[package]]
264+ name = "parking_lot"
265+ version = "0.12.3"
266+ source = "registry+https://github.com/rust-lang/crates.io-index"
267+ checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
268+ dependencies = [
269+ "lock_api",
270+ "parking_lot_core",
271+ ]
272+
273+ [[package]]
274+ name = "parking_lot_core"
275+ version = "0.9.10"
276+ source = "registry+https://github.com/rust-lang/crates.io-index"
277+ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
278+ dependencies = [
279+ "cfg-if",
280+ "libc",
281+ "redox_syscall",
282+ "smallvec",
283+ "windows-targets",
284 ]
285
286 [[package]]
287 @@ -503,6 +658,15 @@ dependencies = [
288 ]
289
290 [[package]]
291+ name = "redox_syscall"
292+ version = "0.5.10"
293+ source = "registry+https://github.com/rust-lang/crates.io-index"
294+ checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
295+ dependencies = [
296+ "bitflags",
297+ ]
298+
299+ [[package]]
300 name = "regex"
301 version = "1.11.1"
302 source = "registry+https://github.com/rust-lang/crates.io-index"
303 @@ -550,6 +714,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
304 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
305
306 [[package]]
307+ name = "scopeguard"
308+ version = "1.2.0"
309+ source = "registry+https://github.com/rust-lang/crates.io-index"
310+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
311+
312+ [[package]]
313 name = "serde"
314 version = "1.0.219"
315 source = "registry+https://github.com/rust-lang/crates.io-index"
316 @@ -604,6 +774,33 @@ dependencies = [
317 ]
318
319 [[package]]
320+ name = "sharded-slab"
321+ version = "0.1.7"
322+ source = "registry+https://github.com/rust-lang/crates.io-index"
323+ checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
324+ dependencies = [
325+ "lazy_static",
326+ ]
327+
328+ [[package]]
329+ name = "signal-hook-registry"
330+ version = "1.4.2"
331+ source = "registry+https://github.com/rust-lang/crates.io-index"
332+ checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
333+ dependencies = [
334+ "libc",
335+ ]
336+
337+ [[package]]
338+ name = "slab"
339+ version = "0.4.9"
340+ source = "registry+https://github.com/rust-lang/crates.io-index"
341+ checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
342+ dependencies = [
343+ "autocfg",
344+ ]
345+
346+ [[package]]
347 name = "smallvec"
348 version = "1.14.0"
349 source = "registry+https://github.com/rust-lang/crates.io-index"
350 @@ -682,15 +879,28 @@ dependencies = [
351 ]
352
353 [[package]]
354+ name = "thread_local"
355+ version = "1.1.8"
356+ source = "registry+https://github.com/rust-lang/crates.io-index"
357+ checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
358+ dependencies = [
359+ "cfg-if",
360+ "once_cell",
361+ ]
362+
363+ [[package]]
364 name = "tokio"
365 version = "1.44.1"
366 source = "registry+https://github.com/rust-lang/crates.io-index"
367 checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
368 dependencies = [
369 "backtrace",
370+ "bytes",
371 "libc",
372 "mio",
373+ "parking_lot",
374 "pin-project-lite",
375+ "signal-hook-registry",
376 "socket2",
377 "tokio-macros",
378 "windows-sys",
379 @@ -724,6 +934,22 @@ dependencies = [
380 ]
381
382 [[package]]
383+ name = "tower-http"
384+ version = "0.6.2"
385+ source = "registry+https://github.com/rust-lang/crates.io-index"
386+ checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
387+ dependencies = [
388+ "bitflags",
389+ "bytes",
390+ "http",
391+ "http-body",
392+ "pin-project-lite",
393+ "tower-layer",
394+ "tower-service",
395+ "tracing",
396+ ]
397+
398+ [[package]]
399 name = "tower-layer"
400 version = "0.3.3"
401 source = "registry+https://github.com/rust-lang/crates.io-index"
402 @@ -765,6 +991,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
403 checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
404 dependencies = [
405 "once_cell",
406+ "valuable",
407+ ]
408+
409+ [[package]]
410+ name = "tracing-log"
411+ version = "0.2.0"
412+ source = "registry+https://github.com/rust-lang/crates.io-index"
413+ checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
414+ dependencies = [
415+ "log",
416+ "once_cell",
417+ "tracing-core",
418+ ]
419+
420+ [[package]]
421+ name = "tracing-subscriber"
422+ version = "0.3.19"
423+ source = "registry+https://github.com/rust-lang/crates.io-index"
424+ checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
425+ dependencies = [
426+ "nu-ansi-term",
427+ "sharded-slab",
428+ "smallvec",
429+ "thread_local",
430+ "tracing-core",
431+ "tracing-log",
432 ]
433
434 [[package]]
435 @@ -774,12 +1026,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
436 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
437
438 [[package]]
439+ name = "uuid"
440+ version = "1.16.0"
441+ source = "registry+https://github.com/rust-lang/crates.io-index"
442+ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
443+ dependencies = [
444+ "getrandom",
445+ ]
446+
447+ [[package]]
448+ name = "valuable"
449+ version = "0.1.1"
450+ source = "registry+https://github.com/rust-lang/crates.io-index"
451+ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
452+
453+ [[package]]
454 name = "wasi"
455 version = "0.11.0+wasi-snapshot-preview1"
456 source = "registry+https://github.com/rust-lang/crates.io-index"
457 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
458
459 [[package]]
460+ name = "wasi"
461+ version = "0.13.3+wasi-0.2.2"
462+ source = "registry+https://github.com/rust-lang/crates.io-index"
463+ checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
464+ dependencies = [
465+ "wit-bindgen-rt",
466+ ]
467+
468+ [[package]]
469+ name = "winapi"
470+ version = "0.3.9"
471+ source = "registry+https://github.com/rust-lang/crates.io-index"
472+ checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
473+ dependencies = [
474+ "winapi-i686-pc-windows-gnu",
475+ "winapi-x86_64-pc-windows-gnu",
476+ ]
477+
478+ [[package]]
479+ name = "winapi-i686-pc-windows-gnu"
480+ version = "0.4.0"
481+ source = "registry+https://github.com/rust-lang/crates.io-index"
482+ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
483+
484+ [[package]]
485+ name = "winapi-x86_64-pc-windows-gnu"
486+ version = "0.4.0"
487+ source = "registry+https://github.com/rust-lang/crates.io-index"
488+ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
489+
490+ [[package]]
491 name = "windows-sys"
492 version = "0.52.0"
493 source = "registry+https://github.com/rust-lang/crates.io-index"
494 @@ -851,3 +1149,12 @@ name = "windows_x86_64_msvc"
495 version = "0.52.6"
496 source = "registry+https://github.com/rust-lang/crates.io-index"
497 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
498+
499+ [[package]]
500+ name = "wit-bindgen-rt"
501+ version = "0.33.0"
502+ source = "registry+https://github.com/rust-lang/crates.io-index"
503+ checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
504+ dependencies = [
505+ "bitflags",
506+ ]
507 diff --git a/Cargo.toml b/Cargo.toml
508index 7a6e130..d47df22 100644
509--- a/Cargo.toml
510+++ b/Cargo.toml
511 @@ -4,7 +4,7 @@ version = "0.1.0"
512 edition = "2024"
513
514 [dependencies]
515- axum = "0.8.1"
516+ axum = { version = "0.8.3", features = ["macros", "query"] }
517 oci-spec = "0.7.1"
518 serde = "1.0.219"
519 serde_json = "1.0.140"
520 @@ -12,3 +12,26 @@ async-trait = "0.1.88"
521 regex = "1.11.1"
522 tracing = { version = "0.1.41", features = ["log"] }
523 thiserror = "2.0.12"
524+ tokio = { version = "1.44.1", features = ["fs"], optional = true }
525+ futures = "0.3.31"
526+ uuid = { version = "1.16.0", features = ["v4"] }
527+ http = "1.3.1"
528+ tower = { version = "0.5.2", features = ["util"] }
529+ bytes = "1.10.1"
530+
531+ [dev-dependencies]
532+ tokio = { version = "1.44.1", features = ["full"] }
533+ tower-http = { version = "0.6.2", features = ["trace", "normalize-path"] }
534+ tracing-subscriber = "0.3.19"
535+
536+ [features]
537+ default = []
538+
539+ storage-fs = [
540+ "tokio"
541+ ]
542+
543+ [[example]]
544+ name = "server"
545+ path = "examples/server.rs"
546+ required-features = ["storage-fs"]
547 diff --git a/LICENSE b/LICENSE
548new file mode 100644
549index 0000000..be3f7b2
550--- /dev/null
551+++ b/LICENSE
552 @@ -0,0 +1,661 @@
553+ GNU AFFERO GENERAL PUBLIC LICENSE
554+ Version 3, 19 November 2007
555+
556+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
557+ Everyone is permitted to copy and distribute verbatim copies
558+ of this license document, but changing it is not allowed.
559+
560+ Preamble
561+
562+ The GNU Affero General Public License is a free, copyleft license for
563+ software and other kinds of works, specifically designed to ensure
564+ cooperation with the community in the case of network server software.
565+
566+ The licenses for most software and other practical works are designed
567+ to take away your freedom to share and change the works. By contrast,
568+ our General Public Licenses are intended to guarantee your freedom to
569+ share and change all versions of a program--to make sure it remains free
570+ software for all its users.
571+
572+ When we speak of free software, we are referring to freedom, not
573+ price. Our General Public Licenses are designed to make sure that you
574+ have the freedom to distribute copies of free software (and charge for
575+ them if you wish), that you receive source code or can get it if you
576+ want it, that you can change the software or use pieces of it in new
577+ free programs, and that you know you can do these things.
578+
579+ Developers that use our General Public Licenses protect your rights
580+ with two steps: (1) assert copyright on the software, and (2) offer
581+ you this License which gives you legal permission to copy, distribute
582+ and/or modify the software.
583+
584+ A secondary benefit of defending all users' freedom is that
585+ improvements made in alternate versions of the program, if they
586+ receive widespread use, become available for other developers to
587+ incorporate. Many developers of free software are heartened and
588+ encouraged by the resulting cooperation. However, in the case of
589+ software used on network servers, this result may fail to come about.
590+ The GNU General Public License permits making a modified version and
591+ letting the public access it on a server without ever releasing its
592+ source code to the public.
593+
594+ The GNU Affero General Public License is designed specifically to
595+ ensure that, in such cases, the modified source code becomes available
596+ to the community. It requires the operator of a network server to
597+ provide the source code of the modified version running there to the
598+ users of that server. Therefore, public use of a modified version, on
599+ a publicly accessible server, gives the public access to the source
600+ code of the modified version.
601+
602+ An older license, called the Affero General Public License and
603+ published by Affero, was designed to accomplish similar goals. This is
604+ a different license, not a version of the Affero GPL, but Affero has
605+ released a new version of the Affero GPL which permits relicensing under
606+ this license.
607+
608+ The precise terms and conditions for copying, distribution and
609+ modification follow.
610+
611+ TERMS AND CONDITIONS
612+
613+ 0. Definitions.
614+
615+ "This License" refers to version 3 of the GNU Affero General Public License.
616+
617+ "Copyright" also means copyright-like laws that apply to other kinds of
618+ works, such as semiconductor masks.
619+
620+ "The Program" refers to any copyrightable work licensed under this
621+ License. Each licensee is addressed as "you". "Licensees" and
622+ "recipients" may be individuals or organizations.
623+
624+ To "modify" a work means to copy from or adapt all or part of the work
625+ in a fashion requiring copyright permission, other than the making of an
626+ exact copy. The resulting work is called a "modified version" of the
627+ earlier work or a work "based on" the earlier work.
628+
629+ A "covered work" means either the unmodified Program or a work based
630+ on the Program.
631+
632+ To "propagate" a work means to do anything with it that, without
633+ permission, would make you directly or secondarily liable for
634+ infringement under applicable copyright law, except executing it on a
635+ computer or modifying a private copy. Propagation includes copying,
636+ distribution (with or without modification), making available to the
637+ public, and in some countries other activities as well.
638+
639+ To "convey" a work means any kind of propagation that enables other
640+ parties to make or receive copies. Mere interaction with a user through
641+ a computer network, with no transfer of a copy, is not conveying.
642+
643+ An interactive user interface displays "Appropriate Legal Notices"
644+ to the extent that it includes a convenient and prominently visible
645+ feature that (1) displays an appropriate copyright notice, and (2)
646+ tells the user that there is no warranty for the work (except to the
647+ extent that warranties are provided), that licensees may convey the
648+ work under this License, and how to view a copy of this License. If
649+ the interface presents a list of user commands or options, such as a
650+ menu, a prominent item in the list meets this criterion.
651+
652+ 1. Source Code.
653+
654+ The "source code" for a work means the preferred form of the work
655+ for making modifications to it. "Object code" means any non-source
656+ form of a work.
657+
658+ A "Standard Interface" means an interface that either is an official
659+ standard defined by a recognized standards body, or, in the case of
660+ interfaces specified for a particular programming language, one that
661+ is widely used among developers working in that language.
662+
663+ The "System Libraries" of an executable work include anything, other
664+ than the work as a whole, that (a) is included in the normal form of
665+ packaging a Major Component, but which is not part of that Major
666+ Component, and (b) serves only to enable use of the work with that
667+ Major Component, or to implement a Standard Interface for which an
668+ implementation is available to the public in source code form. A
669+ "Major Component", in this context, means a major essential component
670+ (kernel, window system, and so on) of the specific operating system
671+ (if any) on which the executable work runs, or a compiler used to
672+ produce the work, or an object code interpreter used to run it.
673+
674+ The "Corresponding Source" for a work in object code form means all
675+ the source code needed to generate, install, and (for an executable
676+ work) run the object code and to modify the work, including scripts to
677+ control those activities. However, it does not include the work's
678+ System Libraries, or general-purpose tools or generally available free
679+ programs which are used unmodified in performing those activities but
680+ which are not part of the work. For example, Corresponding Source
681+ includes interface definition files associated with source files for
682+ the work, and the source code for shared libraries and dynamically
683+ linked subprograms that the work is specifically designed to require,
684+ such as by intimate data communication or control flow between those
685+ subprograms and other parts of the work.
686+
687+ The Corresponding Source need not include anything that users
688+ can regenerate automatically from other parts of the Corresponding
689+ Source.
690+
691+ The Corresponding Source for a work in source code form is that
692+ same work.
693+
694+ 2. Basic Permissions.
695+
696+ All rights granted under this License are granted for the term of
697+ copyright on the Program, and are irrevocable provided the stated
698+ conditions are met. This License explicitly affirms your unlimited
699+ permission to run the unmodified Program. The output from running a
700+ covered work is covered by this License only if the output, given its
701+ content, constitutes a covered work. This License acknowledges your
702+ rights of fair use or other equivalent, as provided by copyright law.
703+
704+ You may make, run and propagate covered works that you do not
705+ convey, without conditions so long as your license otherwise remains
706+ in force. You may convey covered works to others for the sole purpose
707+ of having them make modifications exclusively for you, or provide you
708+ with facilities for running those works, provided that you comply with
709+ the terms of this License in conveying all material for which you do
710+ not control copyright. Those thus making or running the covered works
711+ for you must do so exclusively on your behalf, under your direction
712+ and control, on terms that prohibit them from making any copies of
713+ your copyrighted material outside their relationship with you.
714+
715+ Conveying under any other circumstances is permitted solely under
716+ the conditions stated below. Sublicensing is not allowed; section 10
717+ makes it unnecessary.
718+
719+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
720+
721+ No covered work shall be deemed part of an effective technological
722+ measure under any applicable law fulfilling obligations under article
723+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
724+ similar laws prohibiting or restricting circumvention of such
725+ measures.
726+
727+ When you convey a covered work, you waive any legal power to forbid
728+ circumvention of technological measures to the extent such circumvention
729+ is effected by exercising rights under this License with respect to
730+ the covered work, and you disclaim any intention to limit operation or
731+ modification of the work as a means of enforcing, against the work's
732+ users, your or third parties' legal rights to forbid circumvention of
733+ technological measures.
734+
735+ 4. Conveying Verbatim Copies.
736+
737+ You may convey verbatim copies of the Program's source code as you
738+ receive it, in any medium, provided that you conspicuously and
739+ appropriately publish on each copy an appropriate copyright notice;
740+ keep intact all notices stating that this License and any
741+ non-permissive terms added in accord with section 7 apply to the code;
742+ keep intact all notices of the absence of any warranty; and give all
743+ recipients a copy of this License along with the Program.
744+
745+ You may charge any price or no price for each copy that you convey,
746+ and you may offer support or warranty protection for a fee.
747+
748+ 5. Conveying Modified Source Versions.
749+
750+ You may convey a work based on the Program, or the modifications to
751+ produce it from the Program, in the form of source code under the
752+ terms of section 4, provided that you also meet all of these conditions:
753+
754+ a) The work must carry prominent notices stating that you modified
755+ it, and giving a relevant date.
756+
757+ b) The work must carry prominent notices stating that it is
758+ released under this License and any conditions added under section
759+ 7. This requirement modifies the requirement in section 4 to
760+ "keep intact all notices".
761+
762+ c) You must license the entire work, as a whole, under this
763+ License to anyone who comes into possession of a copy. This
764+ License will therefore apply, along with any applicable section 7
765+ additional terms, to the whole of the work, and all its parts,
766+ regardless of how they are packaged. This License gives no
767+ permission to license the work in any other way, but it does not
768+ invalidate such permission if you have separately received it.
769+
770+ d) If the work has interactive user interfaces, each must display
771+ Appropriate Legal Notices; however, if the Program has interactive
772+ interfaces that do not display Appropriate Legal Notices, your
773+ work need not make them do so.
774+
775+ A compilation of a covered work with other separate and independent
776+ works, which are not by their nature extensions of the covered work,
777+ and which are not combined with it such as to form a larger program,
778+ in or on a volume of a storage or distribution medium, is called an
779+ "aggregate" if the compilation and its resulting copyright are not
780+ used to limit the access or legal rights of the compilation's users
781+ beyond what the individual works permit. Inclusion of a covered work
782+ in an aggregate does not cause this License to apply to the other
783+ parts of the aggregate.
784+
785+ 6. Conveying Non-Source Forms.
786+
787+ You may convey a covered work in object code form under the terms
788+ of sections 4 and 5, provided that you also convey the
789+ machine-readable Corresponding Source under the terms of this License,
790+ in one of these ways:
791+
792+ a) Convey the object code in, or embodied in, a physical product
793+ (including a physical distribution medium), accompanied by the
794+ Corresponding Source fixed on a durable physical medium
795+ customarily used for software interchange.
796+
797+ b) Convey the object code in, or embodied in, a physical product
798+ (including a physical distribution medium), accompanied by a
799+ written offer, valid for at least three years and valid for as
800+ long as you offer spare parts or customer support for that product
801+ model, to give anyone who possesses the object code either (1) a
802+ copy of the Corresponding Source for all the software in the
803+ product that is covered by this License, on a durable physical
804+ medium customarily used for software interchange, for a price no
805+ more than your reasonable cost of physically performing this
806+ conveying of source, or (2) access to copy the
807+ Corresponding Source from a network server at no charge.
808+
809+ c) Convey individual copies of the object code with a copy of the
810+ written offer to provide the Corresponding Source. This
811+ alternative is allowed only occasionally and noncommercially, and
812+ only if you received the object code with such an offer, in accord
813+ with subsection 6b.
814+
815+ d) Convey the object code by offering access from a designated
816+ place (gratis or for a charge), and offer equivalent access to the
817+ Corresponding Source in the same way through the same place at no
818+ further charge. You need not require recipients to copy the
819+ Corresponding Source along with the object code. If the place to
820+ copy the object code is a network server, the Corresponding Source
821+ may be on a different server (operated by you or a third party)
822+ that supports equivalent copying facilities, provided you maintain
823+ clear directions next to the object code saying where to find the
824+ Corresponding Source. Regardless of what server hosts the
825+ Corresponding Source, you remain obligated to ensure that it is
826+ available for as long as needed to satisfy these requirements.
827+
828+ e) Convey the object code using peer-to-peer transmission, provided
829+ you inform other peers where the object code and Corresponding
830+ Source of the work are being offered to the general public at no
831+ charge under subsection 6d.
832+
833+ A separable portion of the object code, whose source code is excluded
834+ from the Corresponding Source as a System Library, need not be
835+ included in conveying the object code work.
836+
837+ A "User Product" is either (1) a "consumer product", which means any
838+ tangible personal property which is normally used for personal, family,
839+ or household purposes, or (2) anything designed or sold for incorporation
840+ into a dwelling. In determining whether a product is a consumer product,
841+ doubtful cases shall be resolved in favor of coverage. For a particular
842+ product received by a particular user, "normally used" refers to a
843+ typical or common use of that class of product, regardless of the status
844+ of the particular user or of the way in which the particular user
845+ actually uses, or expects or is expected to use, the product. A product
846+ is a consumer product regardless of whether the product has substantial
847+ commercial, industrial or non-consumer uses, unless such uses represent
848+ the only significant mode of use of the product.
849+
850+ "Installation Information" for a User Product means any methods,
851+ procedures, authorization keys, or other information required to install
852+ and execute modified versions of a covered work in that User Product from
853+ a modified version of its Corresponding Source. The information must
854+ suffice to ensure that the continued functioning of the modified object
855+ code is in no case prevented or interfered with solely because
856+ modification has been made.
857+
858+ If you convey an object code work under this section in, or with, or
859+ specifically for use in, a User Product, and the conveying occurs as
860+ part of a transaction in which the right of possession and use of the
861+ User Product is transferred to the recipient in perpetuity or for a
862+ fixed term (regardless of how the transaction is characterized), the
863+ Corresponding Source conveyed under this section must be accompanied
864+ by the Installation Information. But this requirement does not apply
865+ if neither you nor any third party retains the ability to install
866+ modified object code on the User Product (for example, the work has
867+ been installed in ROM).
868+
869+ The requirement to provide Installation Information does not include a
870+ requirement to continue to provide support service, warranty, or updates
871+ for a work that has been modified or installed by the recipient, or for
872+ the User Product in which it has been modified or installed. Access to a
873+ network may be denied when the modification itself materially and
874+ adversely affects the operation of the network or violates the rules and
875+ protocols for communication across the network.
876+
877+ Corresponding Source conveyed, and Installation Information provided,
878+ in accord with this section must be in a format that is publicly
879+ documented (and with an implementation available to the public in
880+ source code form), and must require no special password or key for
881+ unpacking, reading or copying.
882+
883+ 7. Additional Terms.
884+
885+ "Additional permissions" are terms that supplement the terms of this
886+ License by making exceptions from one or more of its conditions.
887+ Additional permissions that are applicable to the entire Program shall
888+ be treated as though they were included in this License, to the extent
889+ that they are valid under applicable law. If additional permissions
890+ apply only to part of the Program, that part may be used separately
891+ under those permissions, but the entire Program remains governed by
892+ this License without regard to the additional permissions.
893+
894+ When you convey a copy of a covered work, you may at your option
895+ remove any additional permissions from that copy, or from any part of
896+ it. (Additional permissions may be written to require their own
897+ removal in certain cases when you modify the work.) You may place
898+ additional permissions on material, added by you to a covered work,
899+ for which you have or can give appropriate copyright permission.
900+
901+ Notwithstanding any other provision of this License, for material you
902+ add to a covered work, you may (if authorized by the copyright holders of
903+ that material) supplement the terms of this License with terms:
904+
905+ a) Disclaiming warranty or limiting liability differently from the
906+ terms of sections 15 and 16 of this License; or
907+
908+ b) Requiring preservation of specified reasonable legal notices or
909+ author attributions in that material or in the Appropriate Legal
910+ Notices displayed by works containing it; or
911+
912+ c) Prohibiting misrepresentation of the origin of that material, or
913+ requiring that modified versions of such material be marked in
914+ reasonable ways as different from the original version; or
915+
916+ d) Limiting the use for publicity purposes of names of licensors or
917+ authors of the material; or
918+
919+ e) Declining to grant rights under trademark law for use of some
920+ trade names, trademarks, or service marks; or
921+
922+ f) Requiring indemnification of licensors and authors of that
923+ material by anyone who conveys the material (or modified versions of
924+ it) with contractual assumptions of liability to the recipient, for
925+ any liability that these contractual assumptions directly impose on
926+ those licensors and authors.
927+
928+ All other non-permissive additional terms are considered "further
929+ restrictions" within the meaning of section 10. If the Program as you
930+ received it, or any part of it, contains a notice stating that it is
931+ governed by this License along with a term that is a further
932+ restriction, you may remove that term. If a license document contains
933+ a further restriction but permits relicensing or conveying under this
934+ License, you may add to a covered work material governed by the terms
935+ of that license document, provided that the further restriction does
936+ not survive such relicensing or conveying.
937+
938+ If you add terms to a covered work in accord with this section, you
939+ must place, in the relevant source files, a statement of the
940+ additional terms that apply to those files, or a notice indicating
941+ where to find the applicable terms.
942+
943+ Additional terms, permissive or non-permissive, may be stated in the
944+ form of a separately written license, or stated as exceptions;
945+ the above requirements apply either way.
946+
947+ 8. Termination.
948+
949+ You may not propagate or modify a covered work except as expressly
950+ provided under this License. Any attempt otherwise to propagate or
951+ modify it is void, and will automatically terminate your rights under
952+ this License (including any patent licenses granted under the third
953+ paragraph of section 11).
954+
955+ However, if you cease all violation of this License, then your
956+ license from a particular copyright holder is reinstated (a)
957+ provisionally, unless and until the copyright holder explicitly and
958+ finally terminates your license, and (b) permanently, if the copyright
959+ holder fails to notify you of the violation by some reasonable means
960+ prior to 60 days after the cessation.
961+
962+ Moreover, your license from a particular copyright holder is
963+ reinstated permanently if the copyright holder notifies you of the
964+ violation by some reasonable means, this is the first time you have
965+ received notice of violation of this License (for any work) from that
966+ copyright holder, and you cure the violation prior to 30 days after
967+ your receipt of the notice.
968+
969+ Termination of your rights under this section does not terminate the
970+ licenses of parties who have received copies or rights from you under
971+ this License. If your rights have been terminated and not permanently
972+ reinstated, you do not qualify to receive new licenses for the same
973+ material under section 10.
974+
975+ 9. Acceptance Not Required for Having Copies.
976+
977+ You are not required to accept this License in order to receive or
978+ run a copy of the Program. Ancillary propagation of a covered work
979+ occurring solely as a consequence of using peer-to-peer transmission
980+ to receive a copy likewise does not require acceptance. However,
981+ nothing other than this License grants you permission to propagate or
982+ modify any covered work. These actions infringe copyright if you do
983+ not accept this License. Therefore, by modifying or propagating a
984+ covered work, you indicate your acceptance of this License to do so.
985+
986+ 10. Automatic Licensing of Downstream Recipients.
987+
988+ Each time you convey a covered work, the recipient automatically
989+ receives a license from the original licensors, to run, modify and
990+ propagate that work, subject to this License. You are not responsible
991+ for enforcing compliance by third parties with this License.
992+
993+ An "entity transaction" is a transaction transferring control of an
994+ organization, or substantially all assets of one, or subdividing an
995+ organization, or merging organizations. If propagation of a covered
996+ work results from an entity transaction, each party to that
997+ transaction who receives a copy of the work also receives whatever
998+ licenses to the work the party's predecessor in interest had or could
999+ give under the previous paragraph, plus a right to possession of the
1000+ Corresponding Source of the work from the predecessor in interest, if
1001+ the predecessor has it or can get it with reasonable efforts.
1002+
1003+ You may not impose any further restrictions on the exercise of the
1004+ rights granted or affirmed under this License. For example, you may
1005+ not impose a license fee, royalty, or other charge for exercise of
1006+ rights granted under this License, and you may not initiate litigation
1007+ (including a cross-claim or counterclaim in a lawsuit) alleging that
1008+ any patent claim is infringed by making, using, selling, offering for
1009+ sale, or importing the Program or any portion of it.
1010+
1011+ 11. Patents.
1012+
1013+ A "contributor" is a copyright holder who authorizes use under this
1014+ License of the Program or a work on which the Program is based. The
1015+ work thus licensed is called the contributor's "contributor version".
1016+
1017+ A contributor's "essential patent claims" are all patent claims
1018+ owned or controlled by the contributor, whether already acquired or
1019+ hereafter acquired, that would be infringed by some manner, permitted
1020+ by this License, of making, using, or selling its contributor version,
1021+ but do not include claims that would be infringed only as a
1022+ consequence of further modification of the contributor version. For
1023+ purposes of this definition, "control" includes the right to grant
1024+ patent sublicenses in a manner consistent with the requirements of
1025+ this License.
1026+
1027+ Each contributor grants you a non-exclusive, worldwide, royalty-free
1028+ patent license under the contributor's essential patent claims, to
1029+ make, use, sell, offer for sale, import and otherwise run, modify and
1030+ propagate the contents of its contributor version.
1031+
1032+ In the following three paragraphs, a "patent license" is any express
1033+ agreement or commitment, however denominated, not to enforce a patent
1034+ (such as an express permission to practice a patent or covenant not to
1035+ sue for patent infringement). To "grant" such a patent license to a
1036+ party means to make such an agreement or commitment not to enforce a
1037+ patent against the party.
1038+
1039+ If you convey a covered work, knowingly relying on a patent license,
1040+ and the Corresponding Source of the work is not available for anyone
1041+ to copy, free of charge and under the terms of this License, through a
1042+ publicly available network server or other readily accessible means,
1043+ then you must either (1) cause the Corresponding Source to be so
1044+ available, or (2) arrange to deprive yourself of the benefit of the
1045+ patent license for this particular work, or (3) arrange, in a manner
1046+ consistent with the requirements of this License, to extend the patent
1047+ license to downstream recipients. "Knowingly relying" means you have
1048+ actual knowledge that, but for the patent license, your conveying the
1049+ covered work in a country, or your recipient's use of the covered work
1050+ in a country, would infringe one or more identifiable patents in that
1051+ country that you have reason to believe are valid.
1052+
1053+ If, pursuant to or in connection with a single transaction or
1054+ arrangement, you convey, or propagate by procuring conveyance of, a
1055+ covered work, and grant a patent license to some of the parties
1056+ receiving the covered work authorizing them to use, propagate, modify
1057+ or convey a specific copy of the covered work, then the patent license
1058+ you grant is automatically extended to all recipients of the covered
1059+ work and works based on it.
1060+
1061+ A patent license is "discriminatory" if it does not include within
1062+ the scope of its coverage, prohibits the exercise of, or is
1063+ conditioned on the non-exercise of one or more of the rights that are
1064+ specifically granted under this License. You may not convey a covered
1065+ work if you are a party to an arrangement with a third party that is
1066+ in the business of distributing software, under which you make payment
1067+ to the third party based on the extent of your activity of conveying
1068+ the work, and under which the third party grants, to any of the
1069+ parties who would receive the covered work from you, a discriminatory
1070+ patent license (a) in connection with copies of the covered work
1071+ conveyed by you (or copies made from those copies), or (b) primarily
1072+ for and in connection with specific products or compilations that
1073+ contain the covered work, unless you entered into that arrangement,
1074+ or that patent license was granted, prior to 28 March 2007.
1075+
1076+ Nothing in this License shall be construed as excluding or limiting
1077+ any implied license or other defenses to infringement that may
1078+ otherwise be available to you under applicable patent law.
1079+
1080+ 12. No Surrender of Others' Freedom.
1081+
1082+ If conditions are imposed on you (whether by court order, agreement or
1083+ otherwise) that contradict the conditions of this License, they do not
1084+ excuse you from the conditions of this License. If you cannot convey a
1085+ covered work so as to satisfy simultaneously your obligations under this
1086+ License and any other pertinent obligations, then as a consequence you may
1087+ not convey it at all. For example, if you agree to terms that obligate you
1088+ to collect a royalty for further conveying from those to whom you convey
1089+ the Program, the only way you could satisfy both those terms and this
1090+ License would be to refrain entirely from conveying the Program.
1091+
1092+ 13. Remote Network Interaction; Use with the GNU General Public License.
1093+
1094+ Notwithstanding any other provision of this License, if you modify the
1095+ Program, your modified version must prominently offer all users
1096+ interacting with it remotely through a computer network (if your version
1097+ supports such interaction) an opportunity to receive the Corresponding
1098+ Source of your version by providing access to the Corresponding Source
1099+ from a network server at no charge, through some standard or customary
1100+ means of facilitating copying of software. This Corresponding Source
1101+ shall include the Corresponding Source for any work covered by version 3
1102+ of the GNU General Public License that is incorporated pursuant to the
1103+ following paragraph.
1104+
1105+ Notwithstanding any other provision of this License, you have
1106+ permission to link or combine any covered work with a work licensed
1107+ under version 3 of the GNU General Public License into a single
1108+ combined work, and to convey the resulting work. The terms of this
1109+ License will continue to apply to the part which is the covered work,
1110+ but the work with which it is combined will remain governed by version
1111+ 3 of the GNU General Public License.
1112+
1113+ 14. Revised Versions of this License.
1114+
1115+ The Free Software Foundation may publish revised and/or new versions of
1116+ the GNU Affero General Public License from time to time. Such new versions
1117+ will be similar in spirit to the present version, but may differ in detail to
1118+ address new problems or concerns.
1119+
1120+ Each version is given a distinguishing version number. If the
1121+ Program specifies that a certain numbered version of the GNU Affero General
1122+ Public License "or any later version" applies to it, you have the
1123+ option of following the terms and conditions either of that numbered
1124+ version or of any later version published by the Free Software
1125+ Foundation. If the Program does not specify a version number of the
1126+ GNU Affero General Public License, you may choose any version ever published
1127+ by the Free Software Foundation.
1128+
1129+ If the Program specifies that a proxy can decide which future
1130+ versions of the GNU Affero General Public License can be used, that proxy's
1131+ public statement of acceptance of a version permanently authorizes you
1132+ to choose that version for the Program.
1133+
1134+ Later license versions may give you additional or different
1135+ permissions. However, no additional obligations are imposed on any
1136+ author or copyright holder as a result of your choosing to follow a
1137+ later version.
1138+
1139+ 15. Disclaimer of Warranty.
1140+
1141+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
1142+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
1143+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
1144+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
1145+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
1146+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
1147+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
1148+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
1149+
1150+ 16. Limitation of Liability.
1151+
1152+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
1153+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
1154+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
1155+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
1156+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
1157+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
1158+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
1159+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
1160+ SUCH DAMAGES.
1161+
1162+ 17. Interpretation of Sections 15 and 16.
1163+
1164+ If the disclaimer of warranty and limitation of liability provided
1165+ above cannot be given local legal effect according to their terms,
1166+ reviewing courts shall apply local law that most closely approximates
1167+ an absolute waiver of all civil liability in connection with the
1168+ Program, unless a warranty or assumption of liability accompanies a
1169+ copy of the Program in return for a fee.
1170+
1171+ END OF TERMS AND CONDITIONS
1172+
1173+ How to Apply These Terms to Your New Programs
1174+
1175+ If you develop a new program, and you want it to be of the greatest
1176+ possible use to the public, the best way to achieve this is to make it
1177+ free software which everyone can redistribute and change under these terms.
1178+
1179+ To do so, attach the following notices to the program. It is safest
1180+ to attach them to the start of each source file to most effectively
1181+ state the exclusion of warranty; and each file should have at least
1182+ the "copyright" line and a pointer to where the full notice is found.
1183+
1184+ <one line to give the program's name and a brief idea of what it does.>
1185+ Copyright (C) <year> <name of author>
1186+
1187+ This program is free software: you can redistribute it and/or modify
1188+ it under the terms of the GNU Affero General Public License as published by
1189+ the Free Software Foundation, either version 3 of the License, or
1190+ (at your option) any later version.
1191+
1192+ This program is distributed in the hope that it will be useful,
1193+ but WITHOUT ANY WARRANTY; without even the implied warranty of
1194+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1195+ GNU Affero General Public License for more details.
1196+
1197+ You should have received a copy of the GNU Affero General Public License
1198+ along with this program. If not, see <https://www.gnu.org/licenses/>.
1199+
1200+ Also add information on how to contact you by electronic and paper mail.
1201+
1202+ If your software can interact with users remotely through a computer
1203+ network, you should also make sure that it provides a way for users to
1204+ get its source. For example, if your program is a web application, its
1205+ interface could display a "Source" link that leads users to an archive
1206+ of the code. There are many ways you could offer source, and different
1207+ solutions will be better for different programs; see section 13 for the
1208+ specific requirements.
1209+
1210+ You should also get your employer (if you work as a programmer) or school,
1211+ if any, to sign a "copyright disclaimer" for the program, if necessary.
1212+ For more information on this, and how to apply and follow the GNU AGPL, see
1213+ <https://www.gnu.org/licenses/>.
1214 diff --git a/README.md b/README.md
1215index 98ba0f3..9b6b5be 100644
1216--- a/README.md
1217+++ b/README.md
1218 @@ -1,5 +1,34 @@
1219 # Papyri
1220
1221 Papyri is a [OCI Distribution Spec](https://github.com/opencontainers/distribution-spec)
1222- compliant container registry that is usable as a standalone application or
1223- consumable as a Rust library.
1224+ compliant container registry that is consumable as a Rust library for use with
1225+ [axum](https://github.com/tokio-rs/axum). An example server is provided for
1226+ illistrative purposes and a full-featured standalone container registry might
1227+ be implemented in the future. This library is intended to be used in
1228+ [Ayllu](https://ayllu-forge.org) but may be useful elsewhere.
1229+
1230+ Note that this code has only been tested for use with Podman.
1231+
1232+
1233+ ## Running the Example
1234+
1235+ First add a new insecure registry to your `/etc/containers/registries.conf`.
1236+
1237+ ```toml
1238+ [[registry]]
1239+ location = "localhost:8700"
1240+ insecure = true
1241+ ```
1242+
1243+ Then run the example server:
1244+
1245+ ```sh
1246+ cargo run --example=server
1247+ ```
1248+ And finally interact with the dev server:
1249+
1250+ ```sh
1251+ podman login localhost:8700
1252+ podman tag alpine:3 localhost:8700/alpine:3
1253+ podman push localhost:8700/alpine:3
1254+ ```
1255 diff --git a/examples/server.rs b/examples/server.rs
1256new file mode 100644
1257index 0000000..d6d8727
1258--- /dev/null
1259+++ b/examples/server.rs
1260 @@ -0,0 +1,51 @@
1261+ use std::{
1262+ error::Error,
1263+ path::{Path, PathBuf},
1264+ str::FromStr,
1265+ sync::Arc,
1266+ };
1267+
1268+ use axum::{
1269+ Router, ServiceExt,
1270+ extract::Request,
1271+ middleware::{self, Next},
1272+ response::Response,
1273+ };
1274+ use papyri::storage_fs::FileSystem;
1275+ use tower::Layer;
1276+ use tower_http::{normalize_path::NormalizePathLayer, trace::TraceLayer};
1277+ use tracing::{Level, info_span};
1278+
1279+ const ADDRESS: &str = "127.0.0.1:8700";
1280+
1281+ #[tokio::main]
1282+ async fn main() -> Result<(), Box<dyn Error>> {
1283+ tracing_subscriber::fmt()
1284+ .compact()
1285+ .with_line_number(true)
1286+ .with_max_level(Level::from_str("DEBUG").unwrap())
1287+ .init();
1288+
1289+ let listener = tokio::net::TcpListener::bind(ADDRESS).await?;
1290+ tracing::info!("listening @ {}", ADDRESS);
1291+
1292+ let fs = FileSystem::new(Path::new("registry"));
1293+ fs.init()?;
1294+
1295+ // Registry middleware must be wrapped with namespace extraction/rewrite.
1296+ let registry = papyri::routes::router(fs);
1297+ let middleware = tower::util::MapRequestLayer::new(papyri::routes::extract_namespace);
1298+
1299+ let router =
1300+ Router::new()
1301+ .nest_service("/v2/", middleware.layer(registry))
1302+ .layer(TraceLayer::new_for_http().make_span_with(
1303+ |req: &Request<_>| {
1304+ info_span!("http_request", method = ?req.method(), uri = req.uri().to_string())
1305+ },
1306+ ))
1307+ .layer(NormalizePathLayer::trim_trailing_slash());
1308+
1309+ axum::serve(listener, router).await?;
1310+ Ok(())
1311+ }
1312 diff --git a/scripts/conformance_test.sh b/scripts/conformance_test.sh
1313new file mode 100755
1314index 0000000..319c4fe
1315--- /dev/null
1316+++ b/scripts/conformance_test.sh
1317 @@ -0,0 +1,23 @@
1318+ #!/bin/sh
1319+ # Build a container from https://github.com/opencontainers/distribution-spec/tree/main/conformance first
1320+
1321+ IMAGE_NAME="conformance:latest"
1322+
1323+ mkdir -p results
1324+
1325+ podman run --rm \
1326+ --net=host \
1327+ -v $(pwd)/results:/results \
1328+ -w /results \
1329+ -e OCI_ROOT_URL="http://localhost:8700" \
1330+ -e OCI_NAMESPACE="myorg/myrepo/a" \
1331+ -e OCI_USERNAME="myuser" \
1332+ -e OCI_PASSWORD="mypass" \
1333+ -e OCI_TEST_PULL=1 \
1334+ -e OCI_TEST_PUSH=0 \
1335+ -e OCI_TEST_CONTENT_DISCOVERY=0 \
1336+ -e OCI_TEST_CONTENT_MANAGEMENT=0 \
1337+ -e OCI_HIDE_SKIPPED_WORKFLOWS=0 \
1338+ -e OCI_DEBUG=0 \
1339+ -e OCI_DELETE_MANIFEST_BEFORE_BLOBS=0 \
1340+ "$IMAGE_NAME"
1341 diff --git a/src/error.rs b/src/error.rs
1342index 45f1daa..724dd18 100644
1343--- a/src/error.rs
1344+++ b/src/error.rs
1345 @@ -45,15 +45,16 @@ pub struct Message {
1346
1347 #[derive(thiserror::Error, Debug)]
1348 pub enum Error {
1349+ #[error("Distribution Error: {{0:?}}")]
1350 Code(Code),
1351+ #[error("IO Failure: {0}")]
1352 Storage(StorageError),
1353- }
1354-
1355- impl std::fmt::Display for Error {
1356- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1357- let msg: Message = self.into();
1358- f.write_str(&format!("{:?}", msg))
1359- }
1360+ #[error("Problem parsing OCI specification: {0}")]
1361+ OciInternal(#[from] oci_spec::OciSpecError),
1362+ #[error("Problem parsing OCI distribution spec: {0}")]
1363+ OciParsing(#[from] oci_spec::distribution::ParseError),
1364+ #[error("Streaming error: {0}")]
1365+ Stream(String),
1366 }
1367
1368 impl From<&Error> for Message {
1369 @@ -79,13 +80,14 @@ impl From<&Error> for Message {
1370 Code::Unsupported => todo!(),
1371 Code::TooManyRequests => todo!(),
1372 },
1373- Error::Storage(error) => {
1374- match error {
1375- StorageError::Unspecified => todo!(),
1376- StorageError::IO => todo!(),
1377- StorageError::NotFound => todo!(),
1378- }
1379+ Error::Storage(error) => Message {
1380+ code: String::from("STORAGE_ERROR"),
1381+ message: String::from("Storage level failure"),
1382+ detail: Some(error.to_string()),
1383 },
1384+ Error::OciInternal(oci_spec_error) => todo!(),
1385+ Error::OciParsing(parse_error) => todo!(),
1386+ Error::Stream(_) => todo!(),
1387 }
1388 }
1389 }
1390 diff --git a/src/handlers.rs b/src/handlers.rs
1391new file mode 100644
1392index 0000000..e5f29c2
1393--- /dev/null
1394+++ b/src/handlers.rs
1395 @@ -0,0 +1,104 @@
1396+ use std::str::FromStr;
1397+
1398+ use axum::{
1399+ body::BodyDataStream, extract::{Path, Query, Request, State}, http::StatusCode, response::Response, Extension, Json
1400+ };
1401+ use bytes::Bytes;
1402+ use futures::{Stream, StreamExt, TryStreamExt};
1403+ use oci_spec::{
1404+ distribution::{Reference, TagList},
1405+ image::{Digest, ImageManifest},
1406+ };
1407+ use serde::Deserialize;
1408+ use serde_json::json;
1409+ use uuid::Uuid;
1410+
1411+ use crate::{Namespace, error::Error, routes::AppState};
1412+
1413+ // pub type Result<T = Json<serde_json::Value>, E = Error> = std::result::Result<T, E>;
1414+
1415+ pub async fn index() -> Result<Json<serde_json::Value>, Error> {
1416+ Ok(Json(json!({})))
1417+ }
1418+
1419+ pub async fn read_manifest(
1420+ State(state): State<AppState>,
1421+ Path((name, reference)): Path<(String, String)>,
1422+ ) -> Result<Json<ImageManifest>, Error> {
1423+ let namespace = Namespace::from_str(&name)?;
1424+ let reference = Reference::from_str(&reference)?;
1425+ let manifest = state.oci.read_manifest(&namespace, &reference).await?;
1426+ Ok(Json(manifest))
1427+ }
1428+
1429+ pub async fn read_blob(
1430+ State(state): State<AppState>,
1431+ Extension(namespace): Extension<Namespace>,
1432+ Path(digest): Path<String>,
1433+ ) {
1434+ println!("Namespace is: {}", namespace);
1435+ }
1436+
1437+ pub async fn stat_blob(
1438+ State(state): State<AppState>,
1439+ Path((name, digest)): Path<(String, String)>,
1440+ ) -> Result<StatusCode, Error> {
1441+ let _ = Namespace::from_str(&name)?;
1442+ let digest = Digest::from_str(&digest)?;
1443+ if !state.oci.exists(&digest).await? {
1444+ Ok(StatusCode::NOT_FOUND)
1445+ } else {
1446+ Ok(StatusCode::OK)
1447+ }
1448+ }
1449+
1450+ pub async fn write_manifest() {}
1451+
1452+ #[derive(Deserialize)]
1453+ pub struct UploadQuery {
1454+ pub digest: String
1455+ }
1456+
1457+ #[axum::debug_handler]
1458+ pub async fn write_blob(
1459+ State(state): State<AppState>,
1460+ Path(upload_uuid): Path<String>,
1461+ Query(query): Query<UploadQuery>,
1462+ req: Request,
1463+ ) -> Result<StatusCode, Error> {
1464+ let digest = Digest::from_str(&query.digest)?;
1465+ let uuid = Uuid::from_str(&upload_uuid)
1466+ .map_err(|_| Error::Code(crate::error::Code::BlobUploadInvalid))?;
1467+ let stream = req.into_body().into_data_stream();
1468+ let stream = stream.map_err(|e| Error::Stream(e.to_string()));
1469+ state.oci.write_blob(Box::pin(stream), &uuid, &digest).await?;
1470+ Ok(StatusCode::OK)
1471+ }
1472+
1473+ pub async fn initiate_blob(
1474+ State(state): State<AppState>,
1475+ Extension(namespace): Extension<Namespace>,
1476+ ) -> Result<Response, Error> {
1477+ let uuid = state.oci.new_blob(&namespace).await?;
1478+ let mut res = Response::new(axum::body::Body::empty());
1479+ let headers = res.headers_mut();
1480+ let uuid_str = uuid.to_string();
1481+ let uri = format!("/v2/upload/{}", uuid_str);
1482+ headers.insert("Location", uri.parse().unwrap());
1483+ Ok(res)
1484+ }
1485+
1486+ pub(crate) async fn read_tags(
1487+ State(state): State<AppState>,
1488+ Path(name): Path<String>,
1489+ ) -> Result<Json<TagList>, Error> {
1490+ let namespace = Namespace::from_str(&name)?;
1491+ let tags = state.oci.list_tags(&namespace).await?;
1492+ Ok(Json(tags))
1493+ }
1494+
1495+ pub async fn read_referrers() {}
1496+
1497+ pub async fn delete_manifest() {}
1498+ pub async fn delete_tag() {}
1499+ pub async fn delete_blob() {}
1500 diff --git a/src/lib.rs b/src/lib.rs
1501index c3e1100..2aac880 100644
1502--- a/src/lib.rs
1503+++ b/src/lib.rs
1504 @@ -1,3 +1,58 @@
1505- pub mod routes;
1506+ use std::{fmt::Display, path::Path, str::FromStr};
1507+
1508+ use error::Error;
1509+ use regex::Regex;
1510+
1511 pub mod error;
1512+ mod handlers;
1513+ pub mod oci_interface;
1514+ pub mod routes;
1515 pub mod storage;
1516+
1517+ #[cfg(feature = "storage-fs")]
1518+ pub mod storage_fs;
1519+
1520+ const NAME_REGEXP_MATCH: &str =
1521+ r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*";
1522+
1523+ // TODO: Consider 255 char namespace limit - hostname length per spec docs
1524+ #[derive(Clone)]
1525+ pub struct Namespace(String);
1526+
1527+ impl Namespace {
1528+ pub fn path(&self) -> &Path {
1529+ Path::new(&self.0)
1530+ }
1531+ }
1532+
1533+ impl FromStr for Namespace {
1534+ type Err = Error;
1535+
1536+ fn from_str(s: &str) -> Result<Self, Self::Err> {
1537+ let regexp = Regex::new(NAME_REGEXP_MATCH).unwrap();
1538+ if regexp.is_match(s) {
1539+ Ok(Namespace(s.to_string()))
1540+ } else {
1541+ Err(Error::Code(crate::error::Code::NameInvalid))
1542+ }
1543+ }
1544+ }
1545+
1546+ impl Display for Namespace {
1547+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1548+ write!(f, "{}", self.0)
1549+ }
1550+ }
1551+
1552+ #[cfg(test)]
1553+ mod test {
1554+ use super::*;
1555+
1556+ #[test]
1557+ fn namespace() {
1558+ Namespace::from_str("fuu").unwrap();
1559+ Namespace::from_str("fuu/bar").unwrap();
1560+ Namespace::from_str("fuu/bar/baz/").unwrap();
1561+ Namespace::from_str("fuu/bar/baz/qux").unwrap();
1562+ }
1563+ }
1564 diff --git a/src/oci_interface.rs b/src/oci_interface.rs
1565new file mode 100644
1566index 0000000..9debc01
1567--- /dev/null
1568+++ b/src/oci_interface.rs
1569 @@ -0,0 +1,91 @@
1570+ use std::{pin::Pin, sync::Arc};
1571+
1572+ use bytes::Bytes;
1573+ use futures::{Stream, StreamExt};
1574+ use oci_spec::{
1575+ distribution::{Reference, TagList},
1576+ image::{Digest, ImageManifest},
1577+ };
1578+ use uuid::Uuid;
1579+
1580+ use crate::{
1581+ Namespace,
1582+ error::Error,
1583+ storage::{Address, Storage},
1584+ };
1585+
1586+ pub mod paths {
1587+ pub const REPOSITORIES: &str = "/repositories";
1588+ }
1589+
1590+ #[derive(Clone)]
1591+ pub(crate) struct OciInterface {
1592+ pub storage: Arc<Box<dyn Storage>>,
1593+ }
1594+
1595+ impl OciInterface {
1596+ pub async fn list_tags(&self, namespace: &Namespace) -> Result<TagList, Error> {
1597+ todo!()
1598+ }
1599+
1600+ pub async fn new_blob(&self, namespace: &Namespace) -> Result<Uuid, Error> {
1601+ let uuid = Uuid::new_v4();
1602+ self.storage
1603+ .write_all(&Address::BlobInit { uuid: &uuid }, &[])
1604+ .await
1605+ .map_err(Error::Storage)?;
1606+ Ok(uuid)
1607+ }
1608+
1609+ pub async fn write_blob<S>(
1610+ &self,
1611+ mut stream: Pin<Box<S>>,
1612+ uuid: &Uuid,
1613+ digest: &Digest,
1614+ ) -> Result<(), Error>
1615+ where
1616+ S: Stream<Item = Result<Bytes, Error>>,
1617+ {
1618+ let tmp_blob = &Address::WriteBlob { uuid };
1619+ while let Some(item) = stream.next().await {
1620+ let chunk = item?.to_vec();
1621+ self.storage
1622+ .write(tmp_blob, chunk.as_slice())
1623+ .await
1624+ .map_err(Error::Storage)?;
1625+ }
1626+ let dst_path = &Address::BlobSpec { digest };
1627+ self.storage
1628+ .mv(tmp_blob, dst_path)
1629+ .await
1630+ .map_err(Error::Storage)?;
1631+ Ok(())
1632+ }
1633+
1634+ pub async fn read_manifest(
1635+ &self,
1636+ namespace: &Namespace,
1637+ reference: &Reference,
1638+ ) -> Result<ImageManifest, Error> {
1639+ let bytes = self
1640+ .storage
1641+ .read_bytes(&Address::Manifest {
1642+ namespace,
1643+ reference,
1644+ })
1645+ .await
1646+ .map_err(Error::Storage)?
1647+ .unwrap();
1648+ let manifest: ImageManifest = serde_json::from_slice(bytes.as_slice()).unwrap();
1649+ Ok(manifest)
1650+ }
1651+
1652+ pub async fn exists(&self, digest: &Digest) -> Result<bool, Error> {
1653+ Ok(self
1654+ .storage
1655+ .stat(&Address::BlobSpec { digest })
1656+ .await
1657+ .map_err(Error::Storage)?
1658+ .is_some())
1659+ }
1660+ }
1661 diff --git a/src/routes.rs b/src/routes.rs
1662index acc33b5..5b48a2a 100644
1663--- a/src/routes.rs
1664+++ b/src/routes.rs
1665 @@ -1,179 +1,95 @@
1666- use std::cell::OnceCell;
1667+ use std::io::Bytes;
1668 use std::str::FromStr;
1669+ use std::sync::Arc;
1670
1671- use axum::Json;
1672- use axum::extract::{MatchedPath, Path, Request};
1673- use axum::response::IntoResponse;
1674- use axum::routing::{delete, head, patch, post, put};
1675+ use axum::extract::Request;
1676+ use axum::routing::{post, put};
1677 use axum::{Router, routing::get};
1678- use core::result::Result as StdResult;
1679- use oci_spec::image::{ImageIndexBuilder, ImageManifest};
1680- use regex::Regex;
1681- use serde::Deserialize;
1682- use serde_json::json;
1683- use tracing::info_span;
1684+ use http::Uri;
1685
1686- use crate::error::Error;
1687+ use crate::Namespace;
1688+ use crate::oci_interface::OciInterface;
1689+ use crate::storage::Storage;
1690
1691- pub type Result<T = Json<serde_json::Value>, E = Error> = std::result::Result<T, E>;
1692-
1693- const NAME_REGEXP_MATCH: &str =
1694- r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*";
1695-
1696- #[derive(Deserialize)]
1697- pub struct Namespace(String);
1698+ #[derive(Clone)]
1699+ pub(crate) struct AppState {
1700+ pub oci: OciInterface,
1701+ }
1702
1703- impl Namespace {
1704- pub fn validate(&self) -> Result<(), Error> {
1705- let regexp = Regex::new(NAME_REGEXP_MATCH).unwrap();
1706- if regexp.is_match(&self.0) {
1707- Ok(())
1708+ pub fn read_ns(uri: &str) -> Option<(Namespace, String)> {
1709+ let mut components: Vec<String> = uri.split("/").map(|cmp| cmp.to_string()).collect();
1710+ components.reverse();
1711+ let stop = components.iter().enumerate().find_map(|(i, entry)| {
1712+ if *entry == "blobs" || *entry == "manifests" || *entry == "tags" {
1713+ Some(i + 1)
1714 } else {
1715- Err(Error::Code(crate::error::Code::NameInvalid))
1716+ None
1717 }
1718+ });
1719+ if let Some(stop) = stop {
1720+ let (route, ns) = components.split_at(stop);
1721+ let mut ns: Vec<String> = ns.iter().map(String::from).collect();
1722+ ns.reverse();
1723+ let ns = ns.join("/");
1724+ let mut route: Vec<String> = route.iter().map(String::from).collect();
1725+ route.reverse();
1726+ let route = format!("/{}", route.join("/"));
1727+ Some((Namespace::from_str(&ns).unwrap(), route))
1728+ } else {
1729+ None
1730 }
1731 }
1732
1733- // Registry conformance applies to the following workflow categories:
1734- //
1735- // 1. **Pull** - Clients are able to pull from the registry
1736- // 2. **Push** - Clients are able to push to the registry
1737- // 3. **Content Discovery** - Clients are able to list or otherwise query the content stored in the registry
1738- // 4. **Content Management** - Clients are able to control the full life-cycle of the content stored in the registry
1739- //
1740-
1741- pub async fn index() -> Result {
1742- Ok(Json(json!({})))
1743- }
1744-
1745- pub mod pull {
1746- pub async fn read_manifest() {}
1747- pub async fn read_blob() {}
1748- }
1749-
1750- pub mod push {
1751- pub async fn write_manifest() {}
1752- pub async fn write_blob() {}
1753- }
1754-
1755- pub mod discovery {
1756- use super::*;
1757-
1758- pub async fn read_tags(Path(name): Path<Namespace>) -> Result {
1759- name.validate()?;
1760- todo!()
1761+ /// Due to the registry spec allowing "namespacing", e.g. fuu/bar/baz/blobs/...
1762+ /// it is required that we rewrite the URI extacting any namespace if it is
1763+ /// specified.
1764+ pub fn extract_namespace(mut req: Request<axum::body::Body>) -> Request<axum::body::Body> {
1765+ let uri_str = req.uri().to_string();
1766+ if let Some((namespace, route)) = read_ns(&uri_str) {
1767+ tracing::info!("Namespace is {}", namespace);
1768+ let extensions = req.extensions_mut();
1769+ extensions.insert(namespace);
1770+ let uri = req.uri_mut();
1771+ *uri = Uri::from_str(&route).unwrap();
1772 }
1773-
1774- pub async fn read_referrers() {}
1775+ req
1776 }
1777
1778- pub mod management {
1779- pub async fn delete_manifest() {}
1780- pub async fn delete_tag() {}
1781- pub async fn delete_blob() {}
1782- }
1783-
1784- pub fn router() -> Router {
1785+ pub fn router(storage: impl Storage + 'static) -> Router {
1786 Router::new()
1787- .route("/v2", get(index))
1788- .route("/v2/", get(index))
1789- .route("/v2/:name/blobs/:digest", get(pull::read_blob))
1790- .route("/v2/:name/blobs/:digest", head(pull::read_blob))
1791- .route("/v2/:name/manifests/:reference", get(pull::read_manifest))
1792- .route("/v2/:name/manifests/:reference", head(pull::read_manifest))
1793- .route("/v2/:name/blobs/uploads", post(push::write_blob))
1794- .route(
1795- "/v2/:name/blobs/uploads/:reference",
1796- patch(push::write_blob),
1797- )
1798- .route("/v2/:name/manifests/:reference", put(push::write_manifest))
1799- .route("/v2/:name/tags/list", get(discovery::read_tags))
1800- .route(
1801- "/v2/:name/manifests/:reference",
1802- delete(management::delete_manifest),
1803- )
1804- .route("/v2/:name/blobs/:digest", delete(management::delete_blob))
1805- // .route("/v2/:name/blobs/uploads", post())
1806+ .route("/v2", get(crate::handlers::index))
1807+ .route("/upload/{uuid}", put(crate::handlers::write_blob))
1808+ .route("/blobs/uploads", post(crate::handlers::initiate_blob))
1809+ // // .route("/{name}/blobs/{digest}", head(crate::handlers::stat_blob))
1810+ // // .route(
1811+ // // "/{name}/manifests/{reference}",
1812+ // // get(crate::handlers::read_manifest),
1813+ // // )
1814+ // // .route(
1815+ // // "/{name}/manifests/{reference}",
1816+ // // head(crate::handlers::read_manifest),
1817+ // // )
1818+ // // .route("/{name}/blobs/uploads", post(crate::handlers::initiate_blob))
1819+ // // .route(
1820+ // // "/{name}/blobs/uploads/{reference}",
1821+ // // patch(crate::handlers::write_blob),
1822+ // // )
1823+ // // .route(
1824+ // // "/{name}/manifests/{reference}",
1825+ // // put(crate::handlers::write_manifest),
1826+ // // )
1827+ // // .route("/{name}/tags/list", get(crate::handlers::read_tags))
1828+ // // .route(
1829+ // // "/{name}/manifests/{reference}",
1830+ // // delete(crate::handlers::delete_manifest),
1831+ // // )
1832+ // // .route(
1833+ // // "/{name}/blobs/{digest}",
1834+ // // delete(crate::handlers::delete_blob),
1835+ // // )
1836+ .with_state(AppState {
1837+ oci: OciInterface {
1838+ storage: Arc::new(Box::new(storage)),
1839+ },
1840+ })
1841 }
1842-
1843- // pub async fn handler_index(req: Request) -> Json<serde_json::Value> {
1844- // req.headers().iter().for_each(|h| {
1845- // let header_value = h.1.to_str().ok();
1846- // tracing::info!(key = h.0.as_str(), value = header_value);
1847- // });
1848- // Json(json!({}))
1849- // }
1850- //
1851- // pub async fn handler_manifest_index() -> Json<serde_json::Value> {
1852- // }
1853-
1854- // async fn serve() {
1855- // let app = Router::new()
1856- // .route("/", get(|| async { "Hello, World!" }))
1857- // .route("/v2", get(handler_index))
1858- // .route("/v2/", get(handler_index))
1859- // .layer(
1860- // TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
1861- // // Log the matched route's path (with placeholders not filled in).
1862- // // Use request.uri() or OriginalUri if you want the real path.
1863- // let matched_path = request
1864- // .extensions()
1865- // .get::<MatchedPath>()
1866- // .map(MatchedPath::as_str);
1867- //
1868- // let actual_path = request.uri().path();
1869- //
1870- // info_span!(
1871- // "http_request",
1872- // method = ?request.method(),
1873- // matched_path,
1874- // actual_path,
1875- // some_other_field = tracing::field::Empty,
1876- // )
1877- // }), // .on_request(|_request: &Request<_>, _span: &Span| {
1878- // // // You can use `_span.record("some_other_field", value)` in one of these
1879- // // // closures to attach a value to the initially empty field in the info_span
1880- // // // created above.
1881- // // })
1882- // // .on_response(|_response: &Response, _latency: Duration, _span: &Span| {
1883- // // // ...
1884- // // })
1885- // // .on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
1886- // // // ...
1887- // // })
1888- // // .on_eos(
1889- // // |_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
1890- // // // ...
1891- // // },
1892- // // )
1893- // // .on_failure(
1894- // // |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
1895- // // // ...
1896- // // },
1897- // // ),
1898- // );
1899- //
1900- // // run our app with hyper, listening globally on port 3000
1901- // tracing::info!("Listening @ 0.0.0.0:3000");
1902- // let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
1903- // axum::serve(listener, app).await.unwrap();
1904- // }
1905- //
1906- // #[tokio::main]
1907- // async fn main() {
1908- // tracing_subscriber::registry()
1909- // .with(
1910- // tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
1911- // // axum logs rejections from built-in extractors with the `axum::rejection`
1912- // // target, at `TRACE` level. `axum::rejection=trace` enables showing those events
1913- // format!(
1914- // "{}=debug,tower_http=debug,axum::rejection=trace",
1915- // env!("CARGO_CRATE_NAME")
1916- // )
1917- // .into()
1918- // }),
1919- // )
1920- // .with(tracing_subscriber::fmt::layer())
1921- // .init();
1922- // serve().await;
1923- // }
1924 diff --git a/src/storage.rs b/src/storage.rs
1925index 65cb4c0..8539feb 100644
1926--- a/src/storage.rs
1927+++ b/src/storage.rs
1928 @@ -1,21 +1,117 @@
1929+ use std::{
1930+ io::Error as IoError,
1931+ path::{Path, PathBuf},
1932+ pin::Pin,
1933+ };
1934+
1935+ use bytes::Bytes;
1936+ use futures::Stream;
1937+ use oci_spec::{distribution::Reference, image::Digest};
1938+
1939+ use crate::Namespace;
1940+
1941 #[derive(thiserror::Error, Debug)]
1942 pub enum Error {
1943+ #[error("Unspecified Storage Error")]
1944 Unspecified,
1945- IO,
1946+ #[error("Storage IO: {0}")]
1947+ Io(#[from] IoError),
1948+ #[error("Object Not Found")]
1949 NotFound,
1950 }
1951
1952- impl std::fmt::Display for Error {
1953- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1954+ pub struct Object;
1955+
1956+ /// Address normalizes parameters into a general path like object which can
1957+ /// be used for retrieval of objects from the storage implementation.
1958+ pub enum Address<'a> {
1959+ TagPath {
1960+ namespace: &'a Namespace,
1961+ },
1962+ Manifest {
1963+ namespace: &'a Namespace,
1964+ reference: &'a Reference,
1965+ },
1966+ BlobSpec {
1967+ digest: &'a Digest,
1968+ },
1969+ BlobInit {
1970+ uuid: &'a uuid::Uuid,
1971+ },
1972+ WriteBlob {
1973+ uuid: &'a uuid::Uuid,
1974+ },
1975+ MoveBlob {
1976+ uuid: &'a uuid::Uuid,
1977+ digest: &'a Digest,
1978+ },
1979+ }
1980+
1981+ impl Address<'_> {
1982+ /// return the relative path for a certain object or directory of objects
1983+ pub fn path(&self) -> PathBuf {
1984 match self {
1985- Error::Unspecified => f.write_str("An unspecified storage error occurred"),
1986- Error::IO => f.write_str("A storage IO error occurred"),
1987- Error::NotFound => f.write_str("Storage resource not found"),
1988+ Address::TagPath { namespace } => {
1989+ Path::new(&format!("repositories/{}/tags", namespace)).to_path_buf()
1990+ }
1991+ Address::BlobSpec { digest } => {
1992+ let digest_str = digest.digest();
1993+ let first_two: String = digest_str.chars().take(2).collect();
1994+ match digest.algorithm() {
1995+ oci_spec::image::DigestAlgorithm::Sha256 => Path::new(&format!(
1996+ "repositories/blobs/sha256/{}/{}/data",
1997+ first_two, digest_str
1998+ ))
1999+ .to_path_buf(),
2000+ oci_spec::image::DigestAlgorithm::Sha384 => todo!(),
2001+ oci_spec::image::DigestAlgorithm::Sha512 => todo!(),
2002+ oci_spec::image::DigestAlgorithm::Other(_) => todo!(),
2003+ _ => todo!(),
2004+ }
2005+ }
2006+ Address::Manifest {
2007+ namespace,
2008+ reference,
2009+ } => {
2010+ todo!()
2011+ }
2012+ Address::BlobInit { uuid } => {
2013+ Path::new(&format!("repositories/temp/{}", uuid)).to_path_buf()
2014+ }
2015+ Address::WriteBlob { uuid } => {
2016+ Path::new(&format!("repositories/temp/{}", uuid)).to_path_buf()
2017+ }
2018+ Address::MoveBlob { uuid, digest } => {
2019+ let digest_str = digest.digest();
2020+ let first_two: String = digest_str.chars().take(2).collect();
2021+ match digest.algorithm() {
2022+ oci_spec::image::DigestAlgorithm::Sha256 => Path::new(&format!(
2023+ "repositories/blobs/sha256/{}/{}/data",
2024+ first_two, digest_str
2025+ ))
2026+ .to_path_buf(),
2027+ oci_spec::image::DigestAlgorithm::Sha384 => todo!(),
2028+ oci_spec::image::DigestAlgorithm::Sha512 => todo!(),
2029+ oci_spec::image::DigestAlgorithm::Other(_) => todo!(),
2030+ _ => todo!(),
2031+ }
2032+ },
2033 }
2034 }
2035 }
2036
2037+ /// The storage trait needs to be implemented for accessing objects from the
2038+ /// platform. This API is based on registry/storage/driver/storagedriver.go in
2039+ /// the distribution codebase.
2040 #[async_trait::async_trait]
2041 pub trait Storage: Sync + Send {
2042- async fn read_tags() -> Result<Vec<String>, Error>;
2043+ /// List a single directory of objects
2044+ async fn list(&self, addr: &Address) -> Result<Vec<Object>, Error>;
2045+ async fn stat(&self, addr: &Address) -> Result<Option<Object>, Error>;
2046+ async fn read_bytes(&self, addr: &Address) -> Result<Option<Vec<u8>>, Error>;
2047+ /// Write bytes to the address, truncating any existing object
2048+ async fn write_all(&self, addr: &Address, bytes: &[u8]) -> Result<(), Error>;
2049+ /// write bytes to a file that has already been created
2050+ async fn write(&self, addr: &Address, bytes: &[u8]) -> Result<(), Error>;
2051+ async fn mv(&self, src: &Address, dst: &Address) -> Result<(), Error>;
2052 }
2053 diff --git a/src/storage_fs.rs b/src/storage_fs.rs
2054index e69de29..460bfb5 100644
2055--- a/src/storage_fs.rs
2056+++ b/src/storage_fs.rs
2057 @@ -0,0 +1,106 @@
2058+ use std::{
2059+ io::Cursor,
2060+ path::{Path, PathBuf},
2061+ pin::Pin,
2062+ };
2063+
2064+ use bytes::Bytes;
2065+ use futures::Stream;
2066+ use tokio::io::AsyncWriteExt;
2067+
2068+ use crate::storage::{Address, Error, Object, Storage};
2069+
2070+ #[derive(Clone)]
2071+ pub struct FileSystem {
2072+ base: PathBuf,
2073+ }
2074+
2075+ impl FileSystem {
2076+ pub fn new(path: &Path) -> Self {
2077+ Self {
2078+ base: path.to_path_buf(),
2079+ }
2080+ }
2081+
2082+ pub fn init(&self) -> Result<(), Error> {
2083+ std::fs::create_dir_all(self.base.as_path())?;
2084+ Ok(())
2085+ }
2086+
2087+ pub async fn ensure_dir(&self, path: &Path) -> Result<(), Error> {
2088+ let parent = path.parent().unwrap();
2089+ tokio::fs::create_dir_all(parent).await?;
2090+ Ok(())
2091+ }
2092+ }
2093+
2094+ #[async_trait::async_trait]
2095+ impl Storage for FileSystem {
2096+ async fn list(&self, addr: &Address) -> Result<Vec<Object>, Error> {
2097+ todo!()
2098+ }
2099+
2100+ async fn stat(&self, addr: &Address) -> Result<Option<Object>, Error> {
2101+ let path = self.base.join(addr.path());
2102+ if tokio::fs::try_exists(&path)
2103+ .await
2104+ .is_ok_and(|exists| exists)
2105+ {
2106+ let md = tokio::fs::metadata(path).await?;
2107+ if md.is_file() {
2108+ Ok(Some(Object))
2109+ } else {
2110+ Ok(None)
2111+ }
2112+ } else {
2113+ Ok(None)
2114+ }
2115+ }
2116+
2117+ async fn read_bytes(&self, addr: &Address) -> Result<Option<Vec<u8>>, Error> {
2118+ if self.stat(addr).await?.is_none() {
2119+ return Ok(None);
2120+ }
2121+ let path = addr.path();
2122+ let bytes = tokio::fs::read(&path).await?;
2123+ Ok(Some(bytes))
2124+ }
2125+
2126+ async fn write_all(&self, addr: &Address, bytes: &[u8]) -> Result<(), Error> {
2127+ let path = addr.path();
2128+ self.ensure_dir(&path).await?;
2129+ let mut fp = tokio::fs::OpenOptions::new()
2130+ .create(true)
2131+ .truncate(true)
2132+ .write(true)
2133+ .open(&path)
2134+ .await?;
2135+ let mut cursor = Cursor::new(bytes);
2136+ fp.write_buf(&mut cursor).await?;
2137+ fp.flush().await?;
2138+ Ok(())
2139+ }
2140+
2141+ async fn write(&self, addr: &Address, bytes: &[u8]) -> Result<(), Error> {
2142+ let path = addr.path();
2143+ let mut fp = tokio::fs::OpenOptions::new()
2144+ .create(false)
2145+ .truncate(false)
2146+ .write(true)
2147+ .open(path)
2148+ .await?;
2149+ let mut cursor = Cursor::new(bytes);
2150+ fp.write_buf(&mut cursor).await?;
2151+ fp.flush().await?;
2152+ Ok(())
2153+ }
2154+
2155+ async fn mv(&self, src: &Address, dst: &Address) -> Result<(), Error> {
2156+ let src_path = src.path();
2157+ let dst_path = dst.path();
2158+ self.ensure_dir(&dst_path).await?;
2159+ tracing::info!("mv {:?} -> {:?}", src_path, dst_path);
2160+ tokio::fs::rename(src_path, dst_path).await?;
2161+ Ok(())
2162+ }
2163+ }