Skip to content

Commit eec8dc6

Browse files
authored
Merge pull request #866 from clojars/tcrawley/dynamic-index-files
2 parents 5622cb1 + 65148c9 commit eec8dc6

File tree

9 files changed

+417
-85
lines changed

9 files changed

+417
-85
lines changed

.circleci/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ jobs:
1111
- POSTGRES_USER=clojars
1212
- POSTGRES_PASSWORD=clojars
1313
- POSTGRES_DB=clojars
14+
- image: minio/minio:RELEASE.2023-04-20T17-56-55Z
15+
command: server /data
16+
environment:
17+
MINIO_ROOT_USER: fake-access-key
18+
MINIO_ROOT_PASSWORD: fake-secret-key
1419
working_directory: ~/clojars
1520
steps:
1621
- checkout

docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ services:
2020
- POSTGRES_DB=clojars
2121
volumes:
2222
- ./data/test-postgres:/var/lib/postgresql/data
23+
minio:
24+
image: minio/minio:RELEASE.2023-04-20T17-56-55Z
25+
command: server /data --console-address ":9090"
26+
ports:
27+
- "9000:9000"
28+
- "9090:9090"
29+
environment:
30+
MINIO_ROOT_USER: fake-access-key
31+
MINIO_ROOT_PASSWORD: fake-secret-key
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(ns clojars.routes.repo-listing
2+
(:require
3+
[clojars.web.repo-listing :as repo-listing]
4+
[compojure.core :as compojure :refer [GET HEAD]]
5+
[ring.util.response :as ring.response]))
6+
7+
(defn- repo-listing
8+
[repo-bucket path]
9+
(-> (repo-listing/index repo-bucket path)
10+
(ring.response/response)
11+
(ring.response/content-type "text/html;charset=utf-8")
12+
;; Instruct fastly to cache this result for 15 minutes
13+
(ring.response/header "Cache-Control" "s-maxage=900")))
14+
15+
(defn routes
16+
[repo-bucket]
17+
(compojure/routes
18+
(GET ["/repo-listing"]
19+
{{:keys [path]} :params}
20+
(repo-listing repo-bucket path))
21+
(HEAD ["/repo-listing"]
22+
{{:keys [path]} :params}
23+
(repo-listing repo-bucket path))))

src/clojars/s3.clj

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
(:require
33
[clojure.java.io :as io]
44
[clojure.string :as str]
5-
[cognitect.aws.client.api :as aws])
5+
[cognitect.aws.client.api :as aws]
6+
[cognitect.aws.credentials :as credentials])
67
(:import
78
(java.io
89
ByteArrayInputStream)
10+
(java.util
11+
Date)
912
(org.apache.commons.io
1013
IOUtils)))
1114

1215
(defprotocol S3Bucket
1316
(-delete-object [client key])
1417
(-get-object-details [client key])
1518
(-get-object-stream [client key])
19+
(-list-entries [client prefix])
1620
(-list-objects [client prefix])
1721
(-put-object [client key stream opts]))
1822

@@ -23,26 +27,27 @@
2327
v))
2428

2529
(defn- list-objects-chunk
26-
[client bucket-name prefix marker]
30+
[client bucket-name prefix delimeter continuation-token]
2731
(let [request (cond-> {:Bucket bucket-name}
28-
prefix (assoc :Prefix prefix)
29-
marker (assoc :Marker marker))]
32+
continuation-token (assoc :ContinuationToken continuation-token)
33+
delimeter (assoc :Delimiter delimeter)
34+
prefix (assoc :Prefix prefix))]
3035
(throw-on-error
3136
(aws/invoke client
32-
{:op :ListObjects
37+
{:op :ListObjectsV2
3338
:request request}))))
3439

3540
(defn- list-objects-seq
36-
"Generates a lazy seq of objects, chunked by the API's paging."
37-
[client bucket-name prefix marker]
38-
(let [{:keys [Contents IsTruncated]}
39-
(list-objects-chunk client bucket-name prefix marker)]
41+
"Generates a lazy seq of list-objects results, chunked by the API's paging."
42+
[client bucket-name {:as opts :keys [continuation-token delimeter prefix]}]
43+
(let [{:as result :keys [IsTruncated NextContinuationToken]}
44+
(list-objects-chunk client bucket-name prefix delimeter continuation-token)]
4045
(if IsTruncated
4146
(lazy-seq
42-
(concat Contents
43-
(list-objects-seq client bucket-name prefix
44-
(-> Contents last :Key))))
45-
Contents)))
47+
(cons result
48+
(list-objects-seq client bucket-name
49+
(assoc opts :continuation-token NextContinuationToken))))
50+
[result])))
4651

4752
(defn- strip-etag
4853
"ETags from the s3 api are wrapped in \"s"
@@ -82,8 +87,18 @@
8287
(throw-on-error)
8388
:Body))
8489

90+
(-list-entries [_ prefix]
91+
(sequence
92+
(mapcat #(concat (:CommonPrefixes %) (map strip-etag (:Contents %))))
93+
(list-objects-seq s3 bucket-name {:delimeter "/"
94+
:prefix prefix})))
95+
8596
(-list-objects [_ prefix]
86-
(map strip-etag (list-objects-seq s3 bucket-name prefix nil)))
97+
(sequence
98+
(comp
99+
(mapcat :Contents)
100+
(map strip-etag))
101+
(list-objects-seq s3 bucket-name {:prefix prefix})))
87102

88103
(-put-object [_ key stream opts]
89104
(->> {:op :PutObject
@@ -95,13 +110,29 @@
95110
(throw-on-error))))
96111

97112
(defn s3-client
98-
[bucket]
99-
{:pre [(not (str/blank? bucket))]}
100-
;; Credentials are derived from the instance's role and region comes from the
101-
;; aws.region property, so we don't have to set either here.
102-
(->S3Client (doto (aws/client {:api :s3})
103-
(aws/validate-requests true))
104-
bucket))
113+
;; Credentials are derived from the instance's role when running in
114+
;; production and region comes from the aws.region property, so we don't have
115+
;; to set either here.
116+
([bucket]
117+
(s3-client bucket nil))
118+
;; This arity is only used directly in testing, where we use minio via docker, and we have
119+
;; to override the endpoint and provide credentials
120+
([bucket {:keys [credentials endpoint region]}]
121+
{:pre [(not (str/blank? bucket))]}
122+
(->S3Client
123+
(doto (aws/client
124+
(cond-> {:api :s3}
125+
credentials (assoc :credentials-provider (credentials/basic-credentials-provider credentials))
126+
endpoint (assoc :endpoint-override endpoint)
127+
region (assoc :region region)))
128+
(aws/validate-requests true))
129+
bucket)))
130+
131+
(defn- mock-object-entry
132+
[k bytes]
133+
{:Key k
134+
:Size (count bytes)
135+
:LastModified (Date.)})
105136

106137
(defrecord MockS3Client [state]
107138
S3Bucket
@@ -113,10 +144,27 @@
113144
(-get-object-stream [_ key]
114145
(when-let [data (get @state key)]
115146
(ByteArrayInputStream. data)))
116-
(-list-objects [_ prefix]
147+
(-list-entries [_ prefix]
117148
(->> (keys @state)
118-
(filter (fn [k] (if prefix (.startsWith k prefix) true)))
119-
(map (fn [k] {:Key k}))))
149+
(filter (fn [k]
150+
(if prefix
151+
(.startsWith k prefix)
152+
true)))
153+
(map (fn [k]
154+
(let [k-sans-prefix (if prefix
155+
(subs k (count prefix))
156+
k)
157+
[k-segment & more] (str/split k-sans-prefix #"/")]
158+
(if more
159+
{:Prefix (format "%s%s/" (or prefix "") k-segment)}
160+
(mock-object-entry k (get @state k))))))
161+
(distinct)))
162+
(-list-objects [_ prefix]
163+
(into []
164+
(comp
165+
(filter (fn [k] (if prefix (.startsWith k prefix) true)))
166+
(map (fn [k] (mock-object-entry k (get @state k)))))
167+
(keys @state)))
120168
(-put-object [_ key stream _opts]
121169
(swap! state assoc key (IOUtils/toByteArray stream))))
122170

@@ -139,6 +187,17 @@
139187
[s3 key]
140188
(-get-object-stream s3 key))
141189

190+
(defn list-entries
191+
"Lists the entries in the bucket at the level defined by prefix.
192+
193+
Returns a sequence of intermixed prefix maps (of the form {:Prefix \"some/string/\"})
194+
and object list maps (of the form {:Key \"a-key\", :Size 123, ...}, same as
195+
`list-objects`).
196+
197+
This is used to generate directory listings."
198+
[s3 prefix]
199+
(-list-entries s3 prefix))
200+
142201
(defn list-objects
143202
([s3]
144203
(list-objects s3 nil))

src/clojars/system.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
(component/system-using
9292
{:app [:clojars-app]
9393
:clojars-app [:db :github :gitlab :error-reporter :http-client
94-
:mailer :stats :search :storage]
94+
:mailer :repo-bucket :stats :search :storage]
9595
:http [:app]
9696
:notifications [:db :mailer]
9797
:storage [:error-reporter :repo-bucket]}))))

src/clojars/web.clj

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
[clojars.routes.artifact :as artifact]
1616
[clojars.routes.group :as group]
1717
[clojars.routes.repo :as repo]
18+
[clojars.routes.repo-listing :as repo-listing]
1819
[clojars.routes.session :as session]
1920
[clojars.routes.token :as token]
2021
[clojars.routes.token-breach :as token-breach]
@@ -24,7 +25,7 @@
2425
[clojars.web.common :refer [html-doc]]
2526
[clojars.web.dashboard :refer [dashboard index-page]]
2627
[clojars.web.safe-hiccup :refer [raw]]
27-
[clojars.web.search :refer [search]]
28+
[clojars.web.search :as search]
2829
[clojure.java.io :as io]
2930
[compojure.core :refer [ANY context GET PUT routes]]
3031
[compojure.route :refer [not-found]]
@@ -46,51 +47,54 @@
4647
:error-message "The page query parameter must be an integer."
4748
:status 400})))))
4849

49-
(defn- main-routes [db stats search-obj mailer]
50-
(routes
51-
(GET "/" _
52-
(try-account
53-
#(if %
54-
(dashboard db %)
55-
(index-page db stats %))))
56-
(GET "/search" {:keys [params]}
57-
(try-account
58-
#(let [validated-params (if (:page params)
59-
(assoc params :page (try-parse-page (:page params)))
60-
params)]
61-
(search search-obj % validated-params))))
62-
(GET "/projects" {:keys [params]}
63-
(try-account
64-
#(let [validated-params (if (:page params)
65-
(assoc params :page (try-parse-page (:page params)))
66-
params)]
67-
(browse db % validated-params))))
68-
(GET "/security" []
69-
(try-account
70-
#(html-doc "Security" {:account %}
71-
(raw (slurp (io/resource "security.html"))))))
72-
(GET "/dmca" []
73-
(try-account
74-
#(html-doc "DMCA" {:account %}
75-
(raw (slurp (io/resource "dmca.html"))))))
76-
session/routes
77-
(group/routes db)
78-
(artifact/routes db stats)
79-
;; user routes must go after artifact routes
80-
;; since they both catch /:identifier
81-
(user/routes db mailer)
82-
(verify/routes db)
83-
(token/routes db)
84-
(api/routes db stats)
85-
(GET "/error" _ (throw (Exception. "What!? You really want an error?")))
86-
(PUT "*" _ {:status 405 :headers {} :body "Did you mean to use /repo?"})
87-
(ANY "*" _
88-
(try-account
89-
#(not-found
90-
(html-doc "Page not found" {:account %}
91-
[:div.small-section
92-
[:h1 "Page not found"]
93-
[:p "Thundering typhoons! I think we lost it. Sorry!"]]))))))
50+
(defn- main-routes
51+
[{:as _system :keys [db mailer repo-bucket search stats]}]
52+
(let [db (:spec db)]
53+
(routes
54+
(GET "/" _
55+
(try-account
56+
#(if %
57+
(dashboard db %)
58+
(index-page db stats %))))
59+
(GET "/search" {:keys [params]}
60+
(try-account
61+
#(let [validated-params (if (:page params)
62+
(assoc params :page (try-parse-page (:page params)))
63+
params)]
64+
(search/search search % validated-params))))
65+
(GET "/projects" {:keys [params]}
66+
(try-account
67+
#(let [validated-params (if (:page params)
68+
(assoc params :page (try-parse-page (:page params)))
69+
params)]
70+
(browse db % validated-params))))
71+
(GET "/security" []
72+
(try-account
73+
#(html-doc "Security" {:account %}
74+
(raw (slurp (io/resource "security.html"))))))
75+
(GET "/dmca" []
76+
(try-account
77+
#(html-doc "DMCA" {:account %}
78+
(raw (slurp (io/resource "dmca.html"))))))
79+
session/routes
80+
(repo-listing/routes repo-bucket)
81+
(group/routes db)
82+
(artifact/routes db stats)
83+
;; user routes must go after artifact routes
84+
;; since they both catch /:identifier
85+
(user/routes db mailer)
86+
(verify/routes db)
87+
(token/routes db)
88+
(api/routes db stats)
89+
(GET "/error" _ (throw (Exception. "What!? You really want an error?")))
90+
(PUT "*" _ {:status 405 :headers {} :body "Did you mean to use /repo?"})
91+
(ANY "*" _
92+
(try-account
93+
#(not-found
94+
(html-doc "Page not found" {:account %}
95+
[:div.small-section
96+
[:h1 "Page not found"]
97+
[:p "Thundering typhoons! I think we lost it. Sorry!"]])))))))
9498

9599
(def ^:private defaults-config
96100
(-> ring-defaults/secure-site-defaults
@@ -102,14 +106,13 @@
102106
(dissoc :session)))
103107

104108
(defn clojars-app
105-
[{:keys [db
109+
[{:as system
110+
:keys [db
106111
error-reporter
107112
http-client
108113
github
109114
gitlab
110-
mailer
111115
search
112-
stats
113116
storage]}]
114117
(let [db (:spec db)]
115118
(routes
@@ -131,7 +134,7 @@
131134
(-> (token-breach/routes db)
132135
(wrap-exceptions error-reporter)
133136
(log/wrap-request-context))
134-
(-> (main-routes db stats search mailer)
137+
(-> (main-routes system)
135138
(friend/authenticate
136139
{:credential-fn (auth/password-credential-fn db)
137140
:workflows [(auth/interactive-form-with-mfa-workflow)

0 commit comments

Comments
 (0)