+10
-2
.github/workflows/docker-image.yml
+10
-2
.github/workflows/docker-image.yml
···
5
push:
6
branches:
7
- main
8
9
env:
10
REGISTRY: ghcr.io
···
23
steps:
24
- name: Checkout repository
25
uses: actions/checkout@v4
26
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
27
- name: Log in to the Container registry
28
-
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
29
with:
30
registry: ${{ env.REGISTRY }}
31
username: ${{ github.actor }}
32
password: ${{ secrets.GITHUB_TOKEN }}
33
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
34
- name: Extract metadata (tags, labels) for Docker
35
id: meta
···
37
with:
38
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
39
tags: |
40
type=sha
41
type=sha,format=long
42
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
43
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
44
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
45
- name: Build and push Docker image
46
id: push
47
-
uses: docker/build-push-action@v5
48
with:
49
context: .
50
push: true
···
5
push:
6
branches:
7
- main
8
+
tags:
9
+
- 'v*'
10
11
env:
12
REGISTRY: ghcr.io
···
25
steps:
26
- name: Checkout repository
27
uses: actions/checkout@v4
28
+
29
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
30
- name: Log in to the Container registry
31
+
uses: docker/login-action@v3
32
with:
33
registry: ${{ env.REGISTRY }}
34
username: ${{ github.actor }}
35
password: ${{ secrets.GITHUB_TOKEN }}
36
+
37
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
38
- name: Extract metadata (tags, labels) for Docker
39
id: meta
···
41
with:
42
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
43
tags: |
44
+
type=raw,value=latest,enable={{is_default_branch}}
45
type=sha
46
type=sha,format=long
47
+
type=semver,pattern={{version}}
48
+
type=semver,pattern={{major}}.{{minor}}
49
+
50
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
51
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
52
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
53
- name: Build and push Docker image
54
id: push
55
+
uses: docker/build-push-action@v6
56
with:
57
context: .
58
push: true
+10
Caddyfile.postgres
+10
Caddyfile.postgres
+44
-1
README.md
+44
-1
README.md
···
55
docker-compose up -d
56
```
57
58
5. **Get your invite code**
59
60
On first run, an invite code is automatically created. View it with:
···
96
97
### Optional Configuration
98
99
#### SMTP Email Settings
100
```bash
101
COCOON_SMTP_USER="your-smtp-username"
···
107
```
108
109
#### S3 Storage
110
```bash
111
COCOON_S3_BACKUPS_ENABLED=true
112
COCOON_S3_BLOBSTORE_ENABLED=true
113
COCOON_S3_REGION="us-east-1"
114
COCOON_S3_BUCKET="your-bucket"
115
COCOON_S3_ENDPOINT="https://s3.amazonaws.com"
116
COCOON_S3_ACCESS_KEY="your-access-key"
117
COCOON_S3_SECRET_KEY="your-secret-key"
118
```
119
120
### Management Commands
121
···
160
- [x] `com.atproto.repo.getRecord`
161
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
162
- [x] `com.atproto.repo.listRecords`
163
-
- [ ] `com.atproto.repo.listMissingBlobs`
164
165
### Server
166
···
55
docker-compose up -d
56
```
57
58
+
**For PostgreSQL deployment:**
59
+
```bash
60
+
# Add POSTGRES_PASSWORD to your .env file first!
61
+
docker-compose -f docker-compose.postgres.yaml up -d
62
+
```
63
+
64
5. **Get your invite code**
65
66
On first run, an invite code is automatically created. View it with:
···
102
103
### Optional Configuration
104
105
+
#### Database Configuration
106
+
107
+
By default, Cocoon uses SQLite which requires no additional setup. For production deployments with higher traffic, you can use PostgreSQL:
108
+
109
+
```bash
110
+
# Database type: sqlite (default) or postgres
111
+
COCOON_DB_TYPE="postgres"
112
+
113
+
# PostgreSQL connection string (required if db-type is postgres)
114
+
# Format: postgres://user:password@host:port/database?sslmode=disable
115
+
COCOON_DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"
116
+
117
+
# Or use the standard DATABASE_URL environment variable
118
+
DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"
119
+
```
120
+
121
+
For SQLite (default):
122
+
```bash
123
+
COCOON_DB_TYPE="sqlite"
124
+
COCOON_DB_NAME="/data/cocoon/cocoon.db"
125
+
```
126
+
127
+
> **Note**: When using PostgreSQL, database backups to S3 are not handled by Cocoon. Use `pg_dump` or your database provider's backup solution instead.
128
+
129
#### SMTP Email Settings
130
```bash
131
COCOON_SMTP_USER="your-smtp-username"
···
137
```
138
139
#### S3 Storage
140
+
141
+
Cocoon supports S3-compatible storage for both database backups (SQLite only) and blob storage (images, videos, etc.):
142
+
143
```bash
144
+
# Enable S3 backups (SQLite databases only - hourly backups)
145
COCOON_S3_BACKUPS_ENABLED=true
146
+
147
+
# Enable S3 for blob storage (images, videos, etc.)
148
+
# When enabled, blobs are stored in S3 instead of the database
149
COCOON_S3_BLOBSTORE_ENABLED=true
150
+
151
+
# S3 configuration (works with AWS S3, MinIO, Cloudflare R2, etc.)
152
COCOON_S3_REGION="us-east-1"
153
COCOON_S3_BUCKET="your-bucket"
154
COCOON_S3_ENDPOINT="https://s3.amazonaws.com"
155
COCOON_S3_ACCESS_KEY="your-access-key"
156
COCOON_S3_SECRET_KEY="your-secret-key"
157
```
158
+
159
+
**Blob Storage Options:**
160
+
- `COCOON_S3_BLOBSTORE_ENABLED=false` (default): Blobs stored in the database
161
+
- `COCOON_S3_BLOBSTORE_ENABLED=true`: Blobs stored in S3 bucket under `blobs/{did}/{cid}`
162
163
### Management Commands
164
···
203
- [x] `com.atproto.repo.getRecord`
204
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
205
- [x] `com.atproto.repo.listRecords`
206
+
- [x] `com.atproto.repo.listMissingBlobs` (Not actually functional, but will return a response as if no blobs were missing)
207
208
### Server
209
+36
-1
cmd/cocoon/main.go
+36
-1
cmd/cocoon/main.go
···
17
"github.com/lestrrat-go/jwx/v2/jwk"
18
"github.com/urfave/cli/v2"
19
"golang.org/x/crypto/bcrypt"
20
"gorm.io/driver/sqlite"
21
"gorm.io/gorm"
22
)
···
37
Name: "db-name",
38
Value: "cocoon.db",
39
EnvVars: []string{"COCOON_DB_NAME"},
40
},
41
&cli.StringFlag{
42
Name: "did",
···
157
s, err := server.New(&server.Args{
158
Addr: cmd.String("addr"),
159
DbName: cmd.String("db-name"),
160
Did: cmd.String("did"),
161
Hostname: cmd.String("hostname"),
162
RotationKeyPath: cmd.String("rotation-key-path"),
···
346
}
347
348
func newDb() (*gorm.DB, error) {
349
-
return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
350
}
···
17
"github.com/lestrrat-go/jwx/v2/jwk"
18
"github.com/urfave/cli/v2"
19
"golang.org/x/crypto/bcrypt"
20
+
"gorm.io/driver/postgres"
21
"gorm.io/driver/sqlite"
22
"gorm.io/gorm"
23
)
···
38
Name: "db-name",
39
Value: "cocoon.db",
40
EnvVars: []string{"COCOON_DB_NAME"},
41
+
},
42
+
&cli.StringFlag{
43
+
Name: "db-type",
44
+
Value: "sqlite",
45
+
Usage: "Database type: sqlite or postgres",
46
+
EnvVars: []string{"COCOON_DB_TYPE"},
47
+
},
48
+
&cli.StringFlag{
49
+
Name: "database-url",
50
+
Usage: "PostgreSQL connection string (required if db-type is postgres)",
51
+
EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"},
52
},
53
&cli.StringFlag{
54
Name: "did",
···
169
s, err := server.New(&server.Args{
170
Addr: cmd.String("addr"),
171
DbName: cmd.String("db-name"),
172
+
DbType: cmd.String("db-type"),
173
+
DatabaseURL: cmd.String("database-url"),
174
Did: cmd.String("did"),
175
Hostname: cmd.String("hostname"),
176
RotationKeyPath: cmd.String("rotation-key-path"),
···
360
}
361
362
func newDb() (*gorm.DB, error) {
363
+
dbType := os.Getenv("COCOON_DB_TYPE")
364
+
if dbType == "" {
365
+
dbType = "sqlite"
366
+
}
367
+
368
+
switch dbType {
369
+
case "postgres":
370
+
databaseURL := os.Getenv("COCOON_DATABASE_URL")
371
+
if databaseURL == "" {
372
+
databaseURL = os.Getenv("DATABASE_URL")
373
+
}
374
+
if databaseURL == "" {
375
+
return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres")
376
+
}
377
+
return gorm.Open(postgres.Open(databaseURL), &gorm.Config{})
378
+
default:
379
+
dbName := os.Getenv("COCOON_DB_NAME")
380
+
if dbName == "" {
381
+
dbName = "cocoon.db"
382
+
}
383
+
return gorm.Open(sqlite.Open(dbName), &gorm.Config{})
384
+
}
385
}
+158
docker-compose.postgres.yaml
+158
docker-compose.postgres.yaml
···
···
1
+
# Docker Compose with PostgreSQL
2
+
#
3
+
# Usage:
4
+
# docker-compose -f docker-compose.postgres.yaml up -d
5
+
#
6
+
# This file extends the base docker-compose.yaml with a PostgreSQL database.
7
+
# Set the following in your .env file:
8
+
# COCOON_DB_TYPE=postgres
9
+
# POSTGRES_PASSWORD=your-secure-password
10
+
11
+
version: '3.8'
12
+
13
+
services:
14
+
postgres:
15
+
image: postgres:16-alpine
16
+
container_name: cocoon-postgres
17
+
environment:
18
+
POSTGRES_USER: cocoon
19
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
20
+
POSTGRES_DB: cocoon
21
+
volumes:
22
+
- postgres_data:/var/lib/postgresql/data
23
+
healthcheck:
24
+
test: ["CMD-SHELL", "pg_isready -U cocoon -d cocoon"]
25
+
interval: 10s
26
+
timeout: 5s
27
+
retries: 5
28
+
restart: unless-stopped
29
+
30
+
init-keys:
31
+
build:
32
+
context: .
33
+
dockerfile: Dockerfile
34
+
image: ghcr.io/haileyok/cocoon:latest
35
+
container_name: cocoon-init-keys
36
+
volumes:
37
+
- ./keys:/keys
38
+
- ./data:/data/cocoon
39
+
- ./init-keys.sh:/init-keys.sh:ro
40
+
environment:
41
+
COCOON_DID: ${COCOON_DID}
42
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
43
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
44
+
COCOON_JWK_PATH: /keys/jwk.key
45
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
46
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
47
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
48
+
entrypoint: ["/bin/sh", "/init-keys.sh"]
49
+
restart: "no"
50
+
51
+
cocoon:
52
+
build:
53
+
context: .
54
+
dockerfile: Dockerfile
55
+
image: ghcr.io/haileyok/cocoon:latest
56
+
container_name: cocoon-pds
57
+
depends_on:
58
+
init-keys:
59
+
condition: service_completed_successfully
60
+
postgres:
61
+
condition: service_healthy
62
+
ports:
63
+
- "8080:8080"
64
+
volumes:
65
+
- ./data:/data/cocoon
66
+
- ./keys/rotation.key:/keys/rotation.key:ro
67
+
- ./keys/jwk.key:/keys/jwk.key:ro
68
+
environment:
69
+
# Required settings
70
+
COCOON_DID: ${COCOON_DID}
71
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
72
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
73
+
COCOON_JWK_PATH: /keys/jwk.key
74
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
75
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
76
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
77
+
COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET}
78
+
79
+
# Database configuration - PostgreSQL
80
+
COCOON_ADDR: ":8080"
81
+
COCOON_DB_TYPE: postgres
82
+
COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable
83
+
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
84
+
85
+
# Optional: SMTP settings for email
86
+
COCOON_SMTP_USER: ${COCOON_SMTP_USER:-}
87
+
COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-}
88
+
COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-}
89
+
COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-}
90
+
COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-}
91
+
COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-}
92
+
93
+
# Optional: S3 configuration
94
+
COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false}
95
+
COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false}
96
+
COCOON_S3_REGION: ${COCOON_S3_REGION:-}
97
+
COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-}
98
+
COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-}
99
+
COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-}
100
+
COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-}
101
+
102
+
# Optional: Fallback proxy
103
+
COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-}
104
+
restart: unless-stopped
105
+
healthcheck:
106
+
test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"]
107
+
interval: 30s
108
+
timeout: 10s
109
+
retries: 3
110
+
start_period: 40s
111
+
112
+
create-invite:
113
+
build:
114
+
context: .
115
+
dockerfile: Dockerfile
116
+
image: ghcr.io/haileyok/cocoon:latest
117
+
container_name: cocoon-create-invite
118
+
volumes:
119
+
- ./keys:/keys
120
+
- ./create-initial-invite.sh:/create-initial-invite.sh:ro
121
+
environment:
122
+
COCOON_DID: ${COCOON_DID}
123
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
124
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
125
+
COCOON_JWK_PATH: /keys/jwk.key
126
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
127
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
128
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
129
+
COCOON_DB_TYPE: postgres
130
+
COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable
131
+
depends_on:
132
+
cocoon:
133
+
condition: service_healthy
134
+
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
135
+
restart: "no"
136
+
137
+
caddy:
138
+
image: caddy:2-alpine
139
+
container_name: cocoon-caddy
140
+
ports:
141
+
- "80:80"
142
+
- "443:443"
143
+
volumes:
144
+
- ./Caddyfile.postgres:/etc/caddy/Caddyfile:ro
145
+
- caddy_data:/data
146
+
- caddy_config:/config
147
+
restart: unless-stopped
148
+
environment:
149
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
150
+
CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-}
151
+
152
+
volumes:
153
+
postgres_data:
154
+
driver: local
155
+
caddy_data:
156
+
driver: local
157
+
caddy_config:
158
+
driver: local
+6
-2
docker-compose.yaml
+6
-2
docker-compose.yaml
···
49
50
# Server configuration
51
COCOON_ADDR: ":8080"
52
-
COCOON_DB_NAME: /data/cocoon/cocoon.db
53
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
54
55
# Optional: SMTP settings for email
···
97
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
98
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
99
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
100
-
COCOON_DB_NAME: /data/cocoon/cocoon.db
101
depends_on:
102
- init-keys
103
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
···
49
50
# Server configuration
51
COCOON_ADDR: ":8080"
52
+
COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite}
53
+
COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db}
54
+
COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-}
55
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
56
57
# Optional: SMTP settings for email
···
99
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
100
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
101
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
102
+
COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite}
103
+
COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db}
104
+
COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-}
105
depends_on:
106
- init-keys
107
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
+21
server/handle_repo_list_missing_blobs.go
+21
server/handle_repo_list_missing_blobs.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/labstack/echo/v4"
5
+
)
6
+
7
+
type ComAtprotoRepoListMissingBlobsResponse struct {
8
+
Cursor *string `json:"cursor,omitempty"`
9
+
Blobs []ComAtprotoRepoListMissingBlobsRecordBlob `json:"blobs"`
10
+
}
11
+
12
+
type ComAtprotoRepoListMissingBlobsRecordBlob struct {
13
+
Cid string `json:"cid"`
14
+
RecordUri string `json:"recordUri"`
15
+
}
16
+
17
+
func (s *Server) handleListMissingBlobs(e echo.Context) error {
18
+
return e.JSON(200, ComAtprotoRepoListMissingBlobsResponse{
19
+
Blobs: []ComAtprotoRepoListMissingBlobsRecordBlob{},
20
+
})
21
+
}
+34
-3
server/server.go
+34
-3
server/server.go
···
43
"github.com/labstack/echo/v4"
44
"github.com/labstack/echo/v4/middleware"
45
slogecho "github.com/samber/slog-echo"
46
"gorm.io/driver/sqlite"
47
"gorm.io/gorm"
48
)
···
82
requestCrawlMu sync.Mutex
83
84
dbName string
85
s3Config *S3Config
86
}
87
88
type Args struct {
89
Addr string
90
DbName string
91
Logger *slog.Logger
92
Version string
93
Did string
···
288
IdleTimeout: 5 * time.Minute,
289
}
290
291
-
gdb, err := gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
292
-
if err != nil {
293
-
return nil, err
294
}
295
dbw := db.NewDB(gdb)
296
···
363
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
364
365
dbName: args.DbName,
366
s3Config: args.S3Config,
367
368
oauthProvider: provider.NewProvider(provider.Args{
···
429
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
430
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
431
s.echo.GET("/xrpc/com.atproto.repo.listRecords", s.handleListRecords)
432
s.echo.GET("/xrpc/com.atproto.repo.getRecord", s.handleRepoGetRecord)
433
s.echo.GET("/xrpc/com.atproto.sync.getRecord", s.handleSyncGetRecord)
434
s.echo.GET("/xrpc/com.atproto.sync.getBlocks", s.handleGetBlocks)
···
569
}
570
571
func (s *Server) doBackup() {
572
start := time.Now()
573
574
s.logger.Info("beginning backup to s3...")
···
43
"github.com/labstack/echo/v4"
44
"github.com/labstack/echo/v4/middleware"
45
slogecho "github.com/samber/slog-echo"
46
+
"gorm.io/driver/postgres"
47
"gorm.io/driver/sqlite"
48
"gorm.io/gorm"
49
)
···
83
requestCrawlMu sync.Mutex
84
85
dbName string
86
+
dbType string
87
s3Config *S3Config
88
}
89
90
type Args struct {
91
Addr string
92
DbName string
93
+
DbType string
94
+
DatabaseURL string
95
Logger *slog.Logger
96
Version string
97
Did string
···
292
IdleTimeout: 5 * time.Minute,
293
}
294
295
+
dbType := args.DbType
296
+
if dbType == "" {
297
+
dbType = "sqlite"
298
+
}
299
+
300
+
var gdb *gorm.DB
301
+
var err error
302
+
switch dbType {
303
+
case "postgres":
304
+
if args.DatabaseURL == "" {
305
+
return nil, fmt.Errorf("database-url must be set when using postgres")
306
+
}
307
+
gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{})
308
+
if err != nil {
309
+
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
310
+
}
311
+
args.Logger.Info("connected to PostgreSQL database")
312
+
default:
313
+
gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
314
+
if err != nil {
315
+
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
316
+
}
317
+
args.Logger.Info("connected to SQLite database", "path", args.DbName)
318
}
319
dbw := db.NewDB(gdb)
320
···
387
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
388
389
dbName: args.DbName,
390
+
dbType: dbType,
391
s3Config: args.S3Config,
392
393
oauthProvider: provider.NewProvider(provider.Args{
···
454
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
455
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
456
s.echo.GET("/xrpc/com.atproto.repo.listRecords", s.handleListRecords)
457
+
s.echo.GET("/xrpc/com.atproto.repo.listMissingBlobs", s.handleListMissingBlobs)
458
s.echo.GET("/xrpc/com.atproto.repo.getRecord", s.handleRepoGetRecord)
459
s.echo.GET("/xrpc/com.atproto.sync.getRecord", s.handleSyncGetRecord)
460
s.echo.GET("/xrpc/com.atproto.sync.getBlocks", s.handleGetBlocks)
···
595
}
596
597
func (s *Server) doBackup() {
598
+
if s.dbType == "postgres" {
599
+
s.logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)")
600
+
return
601
+
}
602
+
603
start := time.Now()
604
605
s.logger.Info("beginning backup to s3...")