FossilRepo

Remove Litestream from Dockerfile (EFS handles persistence)

lmata 2026-04-07 05:24 trunk
Commit 5d78642576b16e7632bb6f18c96cc02e04519f583d13fbcb3035d0472e547e09
--- a/docker/litestream-ecs.yml
+++ b/docker/litestream-ecs.yml
@@ -0,0 +1,8 @@
1
+# Litestream ECS config — uses IAM task role for S3 auth (no keys needed)
2
+dbs:
3
+ - path: /data/repos/*.fossil
4
+ replicas:
5
+ - type: s3
6
+ bucket: ${AWS_STORAGE_BUCKET_NAME}
7
+ path: litestream
8
+ region: ${AWS_DEFAULT_REGION}
--- a/docker/litestream-ecs.yml
+++ b/docker/litestream-ecs.yml
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
--- a/docker/litestream-ecs.yml
+++ b/docker/litestream-ecs.yml
@@ -0,0 +1,8 @@
1 # Litestream ECS config — uses IAM task role for S3 auth (no keys needed)
2 dbs:
3 - path: /data/repos/*.fossil
4 replicas:
5 - type: s3
6 bucket: ${AWS_STORAGE_BUCKET_NAME}
7 path: litestream
8 region: ${AWS_DEFAULT_REGION}
+8 -6
--- fossil/cli.py
+++ fossil/cli.py
@@ -248,19 +248,19 @@
248248
return {"success": True, "public_key": pub_key, "fingerprint": fingerprint}
249249
except Exception as e:
250250
return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
251251
return {"success": False, "public_key": "", "fingerprint": ""}
252252
253
- def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "") -> tuple[bytes, str]:
253
+ def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "", localauth: bool = True) -> tuple[bytes, str]:
254254
"""Proxy a single Fossil HTTP sync request via CGI mode.
255255
256
- Runs ``fossil http <repo_path> --localauth`` with the request piped to
257
- stdin. Fossil writes a full HTTP response (headers + body) to stdout;
256
+ Runs ``fossil http <repo_path>`` with the request piped to stdin.
257
+ Fossil writes a full HTTP response (headers + body) to stdout;
258258
we split the two apart and return (response_body, response_content_type).
259259
260
- ``--localauth`` grants full permissions because Django handles auth
261
- before this method is ever called.
260
+ When *localauth* is True, ``--localauth`` grants full push permissions.
261
+ When False, only anonymous pull/clone is allowed (for public repos).
262262
"""
263263
import os
264264
265265
env = {
266266
**os.environ,
@@ -273,11 +273,13 @@
273273
"HTTP_HOST": "localhost",
274274
"GATEWAY_INTERFACE": "CGI/1.1",
275275
"SERVER_PROTOCOL": "HTTP/1.1",
276276
}
277277
278
- cmd = [self.binary, "http", str(repo_path), "--localauth"]
278
+ cmd = [self.binary, "http", str(repo_path)]
279
+ if localauth:
280
+ cmd.append("--localauth")
279281
280282
try:
281283
result = subprocess.run(
282284
cmd,
283285
input=request_body,
284286
--- fossil/cli.py
+++ fossil/cli.py
@@ -248,19 +248,19 @@
248 return {"success": True, "public_key": pub_key, "fingerprint": fingerprint}
249 except Exception as e:
250 return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
251 return {"success": False, "public_key": "", "fingerprint": ""}
252
253 def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "") -> tuple[bytes, str]:
254 """Proxy a single Fossil HTTP sync request via CGI mode.
255
256 Runs ``fossil http <repo_path> --localauth`` with the request piped to
257 stdin. Fossil writes a full HTTP response (headers + body) to stdout;
258 we split the two apart and return (response_body, response_content_type).
259
260 ``--localauth`` grants full permissions because Django handles auth
261 before this method is ever called.
262 """
263 import os
264
265 env = {
266 **os.environ,
@@ -273,11 +273,13 @@
273 "HTTP_HOST": "localhost",
274 "GATEWAY_INTERFACE": "CGI/1.1",
275 "SERVER_PROTOCOL": "HTTP/1.1",
276 }
277
278 cmd = [self.binary, "http", str(repo_path), "--localauth"]
 
 
279
280 try:
281 result = subprocess.run(
282 cmd,
283 input=request_body,
284
--- fossil/cli.py
+++ fossil/cli.py
@@ -248,19 +248,19 @@
248 return {"success": True, "public_key": pub_key, "fingerprint": fingerprint}
249 except Exception as e:
250 return {"success": False, "public_key": "", "fingerprint": "", "error": str(e)}
251 return {"success": False, "public_key": "", "fingerprint": ""}
252
253 def http_proxy(self, repo_path: Path, request_body: bytes, content_type: str = "", localauth: bool = True) -> tuple[bytes, str]:
254 """Proxy a single Fossil HTTP sync request via CGI mode.
255
256 Runs ``fossil http <repo_path>`` with the request piped to stdin.
257 Fossil writes a full HTTP response (headers + body) to stdout;
258 we split the two apart and return (response_body, response_content_type).
259
260 When *localauth* is True, ``--localauth`` grants full push permissions.
261 When False, only anonymous pull/clone is allowed (for public repos).
262 """
263 import os
264
265 env = {
266 **os.environ,
@@ -273,11 +273,13 @@
273 "HTTP_HOST": "localhost",
274 "GATEWAY_INTERFACE": "CGI/1.1",
275 "SERVER_PROTOCOL": "HTTP/1.1",
276 }
277
278 cmd = [self.binary, "http", str(repo_path)]
279 if localauth:
280 cmd.append("--localauth")
281
282 try:
283 result = subprocess.run(
284 cmd,
285 input=request_body,
286
--- fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
+++ fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
@@ -1,12 +1,13 @@
11
# Generated by Django 5.2.12 on 2026-04-07 05:02
22
3
-import core.fields
43
import django.db.models.deletion
54
import simple_history.models
65
from django.conf import settings
76
from django.db import migrations, models
7
+
8
+import core.fields
89
910
1011
class Migration(migrations.Migration):
1112
1213
dependencies = [
1314
--- fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
+++ fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
@@ -1,12 +1,13 @@
1 # Generated by Django 5.2.12 on 2026-04-07 05:02
2
3 import core.fields
4 import django.db.models.deletion
5 import simple_history.models
6 from django.conf import settings
7 from django.db import migrations, models
 
 
8
9
10 class Migration(migrations.Migration):
11
12 dependencies = [
13
--- fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
+++ fossil/migrations/0005_alter_gitmirror_auth_credential_and_more.py
@@ -1,12 +1,13 @@
1 # Generated by Django 5.2.12 on 2026-04-07 05:02
2
 
3 import django.db.models.deletion
4 import simple_history.models
5 from django.conf import settings
6 from django.db import migrations, models
7
8 import core.fields
9
10
11 class Migration(migrations.Migration):
12
13 dependencies = [
14
+25 -8
--- fossil/views.py
+++ fossil/views.py
@@ -1133,47 +1133,64 @@
11331133
def fossil_xfer(request, slug):
11341134
"""Proxy Fossil sync protocol (clone/push/pull) through Django.
11351135
11361136
GET — informational page with clone URL.
11371137
POST — pipe the request body through ``fossil http`` in CGI mode.
1138
+
1139
+ Access control:
1140
+ - Public repos: anonymous clone/pull allowed (no --localauth).
1141
+ - Authenticated users with write access: full push/pull (--localauth).
1142
+ - Private/internal repos: require at least read permission.
11381143
"""
1139
- from projects.access import require_project_read, require_project_write
1144
+ from projects.access import can_read_project, can_write_project
11401145
11411146
from .cli import FossilCLI
11421147
11431148
project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
11441149
fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
11451150
11461151
if request.method == "GET":
1147
- require_project_read(request, project)
1152
+ if not can_read_project(request.user, project):
1153
+ from django.core.exceptions import PermissionDenied
1154
+
1155
+ raise PermissionDenied
11481156
clone_url = request.build_absolute_uri()
1157
+ is_public = project.visibility == "public"
1158
+ auth_note = "" if is_public else "<p>Authentication is required.</p>"
11491159
html = (
11501160
f"<html><head><title>{project.name} — Fossil Sync</title></head>"
11511161
f"<body>"
11521162
f"<h1>{project.name}</h1>"
11531163
f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>"
11541164
f"<p>Clone with:</p>"
11551165
f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>"
1156
- f"<p>Authentication is required.</p>"
1166
+ f"{auth_note}"
11571167
f"</body></html>"
11581168
)
11591169
return HttpResponse(html)
11601170
11611171
if request.method == "POST":
1162
- # All POST sync operations (push/pull/clone) require write access for
1163
- # now. We can loosen pull to read-only once we can distinguish the
1164
- # operation from the request payload.
1165
- require_project_write(request, project)
1166
-
11671172
if not fossil_repo.exists_on_disk:
11681173
raise Http404("Repository file not found on disk.")
11691174
1175
+ has_write = can_write_project(request.user, project)
1176
+ has_read = can_read_project(request.user, project)
1177
+
1178
+ if not has_read:
1179
+ from django.core.exceptions import PermissionDenied
1180
+
1181
+ raise PermissionDenied
1182
+
1183
+ # With --localauth, fossil grants full push access (for authenticated
1184
+ # writers). Without it, fossil only allows pull/clone (for anonymous
1185
+ # or read-only users on public repos).
11701186
cli = FossilCLI()
11711187
body, content_type = cli.http_proxy(
11721188
fossil_repo.full_path,
11731189
request.body,
11741190
request.content_type,
1191
+ localauth=has_write,
11751192
)
11761193
return HttpResponse(body, content_type=content_type)
11771194
11781195
return HttpResponse(status=405)
11791196
11801197
--- fossil/views.py
+++ fossil/views.py
@@ -1133,47 +1133,64 @@
1133 def fossil_xfer(request, slug):
1134 """Proxy Fossil sync protocol (clone/push/pull) through Django.
1135
1136 GET — informational page with clone URL.
1137 POST — pipe the request body through ``fossil http`` in CGI mode.
 
 
 
 
 
1138 """
1139 from projects.access import require_project_read, require_project_write
1140
1141 from .cli import FossilCLI
1142
1143 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
1144 fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
1145
1146 if request.method == "GET":
1147 require_project_read(request, project)
 
 
 
1148 clone_url = request.build_absolute_uri()
 
 
1149 html = (
1150 f"<html><head><title>{project.name} — Fossil Sync</title></head>"
1151 f"<body>"
1152 f"<h1>{project.name}</h1>"
1153 f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>"
1154 f"<p>Clone with:</p>"
1155 f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>"
1156 f"<p>Authentication is required.</p>"
1157 f"</body></html>"
1158 )
1159 return HttpResponse(html)
1160
1161 if request.method == "POST":
1162 # All POST sync operations (push/pull/clone) require write access for
1163 # now. We can loosen pull to read-only once we can distinguish the
1164 # operation from the request payload.
1165 require_project_write(request, project)
1166
1167 if not fossil_repo.exists_on_disk:
1168 raise Http404("Repository file not found on disk.")
1169
 
 
 
 
 
 
 
 
 
 
 
1170 cli = FossilCLI()
1171 body, content_type = cli.http_proxy(
1172 fossil_repo.full_path,
1173 request.body,
1174 request.content_type,
 
1175 )
1176 return HttpResponse(body, content_type=content_type)
1177
1178 return HttpResponse(status=405)
1179
1180
--- fossil/views.py
+++ fossil/views.py
@@ -1133,47 +1133,64 @@
1133 def fossil_xfer(request, slug):
1134 """Proxy Fossil sync protocol (clone/push/pull) through Django.
1135
1136 GET — informational page with clone URL.
1137 POST — pipe the request body through ``fossil http`` in CGI mode.
1138
1139 Access control:
1140 - Public repos: anonymous clone/pull allowed (no --localauth).
1141 - Authenticated users with write access: full push/pull (--localauth).
1142 - Private/internal repos: require at least read permission.
1143 """
1144 from projects.access import can_read_project, can_write_project
1145
1146 from .cli import FossilCLI
1147
1148 project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True)
1149 fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True)
1150
1151 if request.method == "GET":
1152 if not can_read_project(request.user, project):
1153 from django.core.exceptions import PermissionDenied
1154
1155 raise PermissionDenied
1156 clone_url = request.build_absolute_uri()
1157 is_public = project.visibility == "public"
1158 auth_note = "" if is_public else "<p>Authentication is required.</p>"
1159 html = (
1160 f"<html><head><title>{project.name} — Fossil Sync</title></head>"
1161 f"<body>"
1162 f"<h1>{project.name}</h1>"
1163 f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>"
1164 f"<p>Clone with:</p>"
1165 f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>"
1166 f"{auth_note}"
1167 f"</body></html>"
1168 )
1169 return HttpResponse(html)
1170
1171 if request.method == "POST":
 
 
 
 
 
1172 if not fossil_repo.exists_on_disk:
1173 raise Http404("Repository file not found on disk.")
1174
1175 has_write = can_write_project(request.user, project)
1176 has_read = can_read_project(request.user, project)
1177
1178 if not has_read:
1179 from django.core.exceptions import PermissionDenied
1180
1181 raise PermissionDenied
1182
1183 # With --localauth, fossil grants full push access (for authenticated
1184 # writers). Without it, fossil only allows pull/clone (for anonymous
1185 # or read-only users on public repos).
1186 cli = FossilCLI()
1187 body, content_type = cli.http_proxy(
1188 fossil_repo.full_path,
1189 request.body,
1190 request.content_type,
1191 localauth=has_write,
1192 )
1193 return HttpResponse(body, content_type=content_type)
1194
1195 return HttpResponse(status=405)
1196
1197

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button