+2
PDSharp.Core/Config.fs
+2
PDSharp.Core/Config.fs
+7
-3
PDSharp.Core/SqliteStore.fs
+7
-3
PDSharp.Core/SqliteStore.fs
···
13
module SqliteStore =
14
15
/// Initialize the database schema
16
-
let initialize (connectionString : string) =
17
-
use conn = new SqliteConnection(connectionString)
18
-
conn.Open()
19
20
conn.Execute("PRAGMA journal_mode=WAL;") |> ignore
21
// TODO: fast, slightly less safe. Keep default (FULL) for now.
22
// conn.Execute("PRAGMA synchronous=NORMAL;") |> ignore
23
···
13
module SqliteStore =
14
15
/// Initialize the database schema
16
+
let initialize (config : AppConfig) =
17
+
use conn = new SqliteConnection(config.SqliteConnectionString)
18
19
+
conn.Open()
20
conn.Execute("PRAGMA journal_mode=WAL;") |> ignore
21
+
22
+
if config.DisableWalAutoCheckpoint then
23
+
conn.Execute("PRAGMA wal_autocheckpoint=0;") |> ignore
24
+
25
// TODO: fast, slightly less safe. Keep default (FULL) for now.
26
// conn.Execute("PRAGMA synchronous=NORMAL;") |> ignore
27
+2
-8
PDSharp.Tests/Handlers.Tests.fs
+2
-8
PDSharp.Tests/Handlers.Tests.fs
···
68
member _.Deserialize<'T>(json : string) = JsonSerializer.Deserialize<'T> json
69
70
member _.Deserialize<'T>(bytes : byte[]) =
71
-
JsonSerializer.Deserialize<'T>(ReadOnlySpan(bytes))
72
73
member _.DeserializeAsync<'T>(stream : Stream) = task { return! JsonSerializer.DeserializeAsync<'T>(stream) }
74
···
115
DidHost = "did:web:pds.example.com"
116
JwtSecret = "secret"
117
SqliteConnectionString = ""
118
BlobStore = Disk "blobs"
119
}
120
···
130
let body = JsonSerializer.Serialize req
131
let ctx = mockContext services body Map.empty
132
let next : HttpFunc = fun _ -> Task.FromResult(None)
133
-
134
let! result = PDSharp.Handlers.Auth.createAccountHandler next ctx
135
-
136
Assert.Equal(200, ctx.Response.StatusCode)
137
138
let store = accountStore :> IAccountStore
···
144
let ``Server.indexHandler returns HTML`` () = task {
145
let ctx = new DefaultHttpContext()
146
let next : HttpFunc = fun _ -> Task.FromResult(None)
147
-
148
let! result = PDSharp.Handlers.Server.indexHandler next ctx
149
-
150
Assert.Equal(200, ctx.Response.StatusCode)
151
Assert.Equal("text/html", ctx.Response.ContentType)
152
}
···
175
}
176
177
let body = JsonSerializer.Serialize(req)
178
-
179
let ctx = mockContext services body Map.empty
180
let next : HttpFunc = fun _ -> Task.FromResult(None)
181
-
182
let! result = PDSharp.Handlers.Repo.createRecordHandler next ctx
183
-
184
Assert.Equal(400, ctx.Response.StatusCode)
185
}
···
68
member _.Deserialize<'T>(json : string) = JsonSerializer.Deserialize<'T> json
69
70
member _.Deserialize<'T>(bytes : byte[]) =
71
+
JsonSerializer.Deserialize<'T>(ReadOnlySpan bytes)
72
73
member _.DeserializeAsync<'T>(stream : Stream) = task { return! JsonSerializer.DeserializeAsync<'T>(stream) }
74
···
115
DidHost = "did:web:pds.example.com"
116
JwtSecret = "secret"
117
SqliteConnectionString = ""
118
+
DisableWalAutoCheckpoint = false
119
BlobStore = Disk "blobs"
120
}
121
···
131
let body = JsonSerializer.Serialize req
132
let ctx = mockContext services body Map.empty
133
let next : HttpFunc = fun _ -> Task.FromResult(None)
134
let! result = PDSharp.Handlers.Auth.createAccountHandler next ctx
135
Assert.Equal(200, ctx.Response.StatusCode)
136
137
let store = accountStore :> IAccountStore
···
143
let ``Server.indexHandler returns HTML`` () = task {
144
let ctx = new DefaultHttpContext()
145
let next : HttpFunc = fun _ -> Task.FromResult(None)
146
let! result = PDSharp.Handlers.Server.indexHandler next ctx
147
Assert.Equal(200, ctx.Response.StatusCode)
148
Assert.Equal("text/html", ctx.Response.ContentType)
149
}
···
172
}
173
174
let body = JsonSerializer.Serialize(req)
175
let ctx = mockContext services body Map.empty
176
let next : HttpFunc = fun _ -> Task.FromResult(None)
177
let! result = PDSharp.Handlers.Repo.createRecordHandler next ctx
178
Assert.Equal(400, ctx.Response.StatusCode)
179
}
+4
-5
PDSharp.Tests/Tests.fs
+4
-5
PDSharp.Tests/Tests.fs
···
18
DidHost = "did:web:example.com"
19
JwtSecret = "test-secret-key-for-testing-only"
20
SqliteConnectionString = "Data Source=:memory:"
21
BlobStore = Disk "blobs"
22
}
23
···
63
let keyPair = generateKey P256
64
let data = Encoding.UTF8.GetBytes("test message")
65
let hash = sha256 data
66
-
67
let signature = sign keyPair hash
68
Assert.True(signature.Length = 64, "Signature should be 64 bytes (R|S)")
69
···
75
let keyPair = generateKey K256
76
let data = Encoding.UTF8.GetBytes("test k256")
77
let hash = sha256 data
78
-
79
let signature = sign keyPair hash
80
Assert.True(signature.Length = 64, "Signature should be 64 bytes")
81
···
117
[<Fact>]
118
let ``CID Generation from Hash`` () =
119
let hash =
120
-
Hex.Decode("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")
121
122
let cid = Cid.FromHash hash
123
Assert.Equal<byte>(0x01uy, cid.Bytes.[0])
···
127
128
[<Fact>]
129
let ``DAG-CBOR Canonical Sorting`` () =
130
-
let m = Map.ofList [ ("b", box 1); ("a", box 2) ]
131
let encoded = DagCbor.encode m
132
let hex = Hex.ToHexString encoded
133
Assert.Equal("a2616102616201", hex)
134
135
[<Fact>]
136
let ``DAG-CBOR Sorting Length vs Bytes`` () =
137
-
let m = Map.ofList [ ("aa", box 1); ("b", box 2) ]
138
let encoded = DagCbor.encode m
139
let hex = Hex.ToHexString encoded
140
Assert.Equal("a261620262616101", hex)
···
18
DidHost = "did:web:example.com"
19
JwtSecret = "test-secret-key-for-testing-only"
20
SqliteConnectionString = "Data Source=:memory:"
21
+
DisableWalAutoCheckpoint = false
22
BlobStore = Disk "blobs"
23
}
24
···
64
let keyPair = generateKey P256
65
let data = Encoding.UTF8.GetBytes("test message")
66
let hash = sha256 data
67
let signature = sign keyPair hash
68
Assert.True(signature.Length = 64, "Signature should be 64 bytes (R|S)")
69
···
75
let keyPair = generateKey K256
76
let data = Encoding.UTF8.GetBytes("test k256")
77
let hash = sha256 data
78
let signature = sign keyPair hash
79
Assert.True(signature.Length = 64, "Signature should be 64 bytes")
80
···
116
[<Fact>]
117
let ``CID Generation from Hash`` () =
118
let hash =
119
+
Hex.Decode "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
120
121
let cid = Cid.FromHash hash
122
Assert.Equal<byte>(0x01uy, cid.Bytes.[0])
···
126
127
[<Fact>]
128
let ``DAG-CBOR Canonical Sorting`` () =
129
+
let m = Map.ofList [ "b", box 1; "a", box 2 ]
130
let encoded = DagCbor.encode m
131
let hex = Hex.ToHexString encoded
132
Assert.Equal("a2616102616201", hex)
133
134
[<Fact>]
135
let ``DAG-CBOR Sorting Length vs Bytes`` () =
136
+
let m = Map.ofList [ "aa", box 1; "b", box 2 ]
137
let encoded = DagCbor.encode m
138
let hex = Hex.ToHexString encoded
139
Assert.Equal("a261620262616101", hex)
+27
-10
PDSharp/Program.fs
+27
-10
PDSharp/Program.fs
···
21
let publicUrl = env "PDSHARP_PublicUrl" "http://localhost:5000"
22
let dbPath = env "PDSHARP_DbPath" "pdsharp.db"
23
24
{
25
PublicUrl = publicUrl
26
DidHost = env "PDSHARP_DidHost" "did:web:localhost"
27
JwtSecret = env "PDSHARP_JwtSecret" "development-secret-do-not-use-in-prod"
28
SqliteConnectionString = $"Data Source={dbPath}"
29
-
BlobStore = Disk "blobs" // Default to disk for now
30
}
31
32
let config = getConfig ()
33
34
-
SqliteStore.initialize config.SqliteConnectionString
35
36
module App =
37
-
let webApp =
38
choose [
39
GET
40
>=> choose [
···
65
RequestErrors.NOT_FOUND "Not Found"
66
]
67
68
-
let configureApp (app : IApplicationBuilder) =
69
app.UseWebSockets() |> ignore
70
-
app.UseGiraffe webApp
71
72
let configureServices (config : AppConfig) (services : IServiceCollection) =
73
services.AddGiraffe() |> ignore
···
77
let accountStore = new SqliteAccountStore(config.SqliteConnectionString)
78
let repoStore = new SqliteRepoStore(config.SqliteConnectionString)
79
80
-
services.AddSingleton<IBlockStore>(blockStore) |> ignore
81
-
services.AddSingleton<IAccountStore>(accountStore) |> ignore
82
-
services.AddSingleton<IRepoStore>(repoStore) |> ignore
83
84
let blobStore : IBlobStore =
85
match config.BlobStore with
86
| Disk path -> new DiskBlobStore(path) :> IBlobStore
87
| S3 s3Config -> new S3BlobStore(s3Config) :> IBlobStore
88
89
-
services.AddSingleton<IBlobStore>(blobStore) |> ignore
90
services.AddSingleton<FirehoseState>(new FirehoseState()) |> ignore
91
services.AddSingleton<SigningKeyStore>(new SigningKeyStore()) |> ignore
92
···
95
Host
96
.CreateDefaultBuilder(args)
97
.ConfigureWebHostDefaults(fun webHostBuilder ->
98
-
webHostBuilder.Configure(configureApp).ConfigureServices(configureServices config)
99
|> ignore)
100
.Build()
101
.Run()
···
21
let publicUrl = env "PDSHARP_PublicUrl" "http://localhost:5000"
22
let dbPath = env "PDSHARP_DbPath" "pdsharp.db"
23
24
+
let disableWalAutoCheckpoint =
25
+
env "PDSHARP_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT" "false" |> bool.Parse
26
+
27
+
let blobStoreConfig =
28
+
match env "PDSHARP_BLOBSTORE_TYPE" "disk" with
29
+
| "s3" ->
30
+
S3 {
31
+
Bucket = env "PDSHARP_S3_BUCKET" "pdsharp-blobs"
32
+
Region = env "PDSHARP_S3_REGION" "us-east-1"
33
+
AccessKey = Option.ofObj (Environment.GetEnvironmentVariable "PDSHARP_S3_ACCESS_KEY")
34
+
SecretKey = Option.ofObj (Environment.GetEnvironmentVariable "PDSHARP_S3_SECRET_KEY")
35
+
ServiceUrl = Option.ofObj (Environment.GetEnvironmentVariable "PDSHARP_S3_SERVICE_URL")
36
+
ForcePathStyle = env "PDSHARP_S3_FORCE_PATH_STYLE" "false" |> bool.Parse
37
+
}
38
+
| _ -> Disk "blobs"
39
+
40
{
41
PublicUrl = publicUrl
42
DidHost = env "PDSHARP_DidHost" "did:web:localhost"
43
JwtSecret = env "PDSHARP_JwtSecret" "development-secret-do-not-use-in-prod"
44
SqliteConnectionString = $"Data Source={dbPath}"
45
+
DisableWalAutoCheckpoint = disableWalAutoCheckpoint
46
+
BlobStore = blobStoreConfig
47
}
48
49
let config = getConfig ()
50
51
+
SqliteStore.initialize config
52
53
module App =
54
+
let appRouter =
55
choose [
56
GET
57
>=> choose [
···
82
RequestErrors.NOT_FOUND "Not Found"
83
]
84
85
+
let webApp (app : IApplicationBuilder) =
86
app.UseWebSockets() |> ignore
87
+
app.UseGiraffe appRouter
88
89
let configureServices (config : AppConfig) (services : IServiceCollection) =
90
services.AddGiraffe() |> ignore
···
94
let accountStore = new SqliteAccountStore(config.SqliteConnectionString)
95
let repoStore = new SqliteRepoStore(config.SqliteConnectionString)
96
97
+
services.AddSingleton<IBlockStore> blockStore |> ignore
98
+
services.AddSingleton<IAccountStore> accountStore |> ignore
99
+
services.AddSingleton<IRepoStore> repoStore |> ignore
100
101
let blobStore : IBlobStore =
102
match config.BlobStore with
103
| Disk path -> new DiskBlobStore(path) :> IBlobStore
104
| S3 s3Config -> new S3BlobStore(s3Config) :> IBlobStore
105
106
+
services.AddSingleton<IBlobStore> blobStore |> ignore
107
services.AddSingleton<FirehoseState>(new FirehoseState()) |> ignore
108
services.AddSingleton<SigningKeyStore>(new SigningKeyStore()) |> ignore
109
···
112
Host
113
.CreateDefaultBuilder(args)
114
.ConfigureWebHostDefaults(fun webHostBuilder ->
115
+
webHostBuilder.Configure(webApp).ConfigureServices(configureServices config)
116
|> ignore)
117
.Build()
118
.Run()
+3
-3
roadmap.txt
+3
-3
roadmap.txt
···
61
--------------------------------------------------------------------------------
62
Milestone J: Storage Backend Configuration
63
--------------------------------------------------------------------------------
64
-
- [ ] Configure SQLite WAL mode (PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT=true)
65
-
- [ ] Implement S3-compatible blobstore adapter (optional via config)
66
-
- [ ] Configure disk-based vs S3-based blob storage selection
67
DoD: PDS runs with S3 blobs (if configured) and SQLite passes Litestream checks
68
--------------------------------------------------------------------------------
69
Milestone K: Backup Automation + Guardrails
···
61
--------------------------------------------------------------------------------
62
Milestone J: Storage Backend Configuration
63
--------------------------------------------------------------------------------
64
+
- [x] Configure SQLite WAL mode (PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT=true)
65
+
- [x] Implement S3-compatible blobstore adapter (optional via config)
66
+
- [x] Configure disk-based vs S3-based blob storage selection
67
DoD: PDS runs with S3 blobs (if configured) and SQLite passes Litestream checks
68
--------------------------------------------------------------------------------
69
Milestone K: Backup Automation + Guardrails