Version 2.1 (#41)
- Add full chapter support (#6) - Add built-in redirect functionality (#39) - Support building Docker containers for release (#38) - Support canonical domain configuration (#37) - Add unit tests for domain/models and integration tests for all three data stores - Convert SQLite storage to use JSON documents, similar to PostgreSQL - Convert admin templates to Giraffe View Engine (from Liquid) - Add .NET 8 support
This commit was merged in pull request #41.
This commit is contained in:
@@ -5,17 +5,17 @@ open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
|
||||
/// Middleware to derive the current web log
|
||||
type WebLogMiddleware (next : RequestDelegate, log : ILogger<WebLogMiddleware>) =
|
||||
type WebLogMiddleware(next: RequestDelegate, log: ILogger<WebLogMiddleware>) =
|
||||
|
||||
/// Is the debug level enabled on the logger?
|
||||
let isDebug = log.IsEnabled LogLevel.Debug
|
||||
|
||||
member _.InvokeAsync (ctx : HttpContext) = task {
|
||||
member _.InvokeAsync(ctx: HttpContext) = task {
|
||||
/// Create the full path of the request
|
||||
let path = $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}"
|
||||
match WebLogCache.tryGet path with
|
||||
| Some webLog ->
|
||||
if isDebug then log.LogDebug $"Resolved web log {WebLogId.toString webLog.Id} for {path}"
|
||||
if isDebug then log.LogDebug $"Resolved web log {webLog.Id} for {path}"
|
||||
ctx.Items["webLog"] <- webLog
|
||||
if PageListCache.exists ctx then () else do! PageListCache.update ctx
|
||||
if CategoryCache.exists ctx then () else do! CategoryCache.update ctx
|
||||
@@ -26,7 +26,32 @@ type WebLogMiddleware (next : RequestDelegate, log : ILogger<WebLogMiddleware>)
|
||||
}
|
||||
|
||||
|
||||
/// Middleware to check redirects for the current web log
|
||||
type RedirectRuleMiddleware(next: RequestDelegate, log: ILogger<RedirectRuleMiddleware>) =
|
||||
|
||||
/// Shorthand for case-insensitive string equality
|
||||
let ciEquals str1 str2 =
|
||||
System.String.Equals(str1, str2, System.StringComparison.InvariantCultureIgnoreCase)
|
||||
|
||||
member _.InvokeAsync(ctx: HttpContext) = task {
|
||||
let path = ctx.Request.Path.Value.ToLower()
|
||||
let matched =
|
||||
WebLogCache.redirectRules ctx.WebLog.Id
|
||||
|> List.tryPick (fun rule ->
|
||||
match rule with
|
||||
| WebLogCache.CachedRedirectRule.Text (urlFrom, urlTo) ->
|
||||
if ciEquals path urlFrom then Some urlTo else None
|
||||
| WebLogCache.CachedRedirectRule.RegEx (regExFrom, patternTo) ->
|
||||
if regExFrom.IsMatch path then Some (regExFrom.Replace(path, patternTo)) else None)
|
||||
match matched with
|
||||
| Some url -> ctx.Response.Redirect(url, permanent = true)
|
||||
| None -> return! next.Invoke ctx
|
||||
}
|
||||
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
open BitBadger.Documents
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open MyWebLog.Data
|
||||
open Newtonsoft.Json
|
||||
@@ -38,43 +63,44 @@ module DataImplementation =
|
||||
open MyWebLog.Converters
|
||||
open RethinkDb.Driver.FSharp
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
|
||||
/// Create an NpgsqlDataSource from the connection string, configuring appropriately
|
||||
let createNpgsqlDataSource (cfg : IConfiguration) =
|
||||
let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL")
|
||||
let _ = builder.UseNodaTime ()
|
||||
let createNpgsqlDataSource (cfg: IConfiguration) =
|
||||
let builder = NpgsqlDataSourceBuilder(cfg.GetConnectionString "PostgreSQL")
|
||||
let _ = builder.UseNodaTime()
|
||||
// let _ = builder.UseLoggerFactory(LoggerFactory.Create(fun it -> it.AddConsole () |> ignore))
|
||||
builder.Build ()
|
||||
(builder.Build >> Postgres.Configuration.useDataSource) ()
|
||||
|
||||
/// Get the configured data implementation
|
||||
let get (sp : IServiceProvider) : IData =
|
||||
let config = sp.GetRequiredService<IConfiguration> ()
|
||||
let get (sp: IServiceProvider) : IData =
|
||||
let config = sp.GetRequiredService<IConfiguration>()
|
||||
let await it = (Async.AwaitTask >> Async.RunSynchronously) it
|
||||
let connStr name = config.GetConnectionString name
|
||||
let hasConnStr name = (connStr >> isNull >> not) name
|
||||
let createSQLite connStr : IData =
|
||||
let log = sp.GetRequiredService<ILogger<SQLiteData>> ()
|
||||
let conn = new SqliteConnection (connStr)
|
||||
Sqlite.Configuration.useConnectionString connStr
|
||||
let log = sp.GetRequiredService<ILogger<SQLiteData>>()
|
||||
let conn = Sqlite.Configuration.dbConn ()
|
||||
log.LogInformation $"Using SQLite database {conn.DataSource}"
|
||||
await (SQLiteData.setUpConnection conn)
|
||||
SQLiteData (conn, log, Json.configure (JsonSerializer.CreateDefault ()))
|
||||
SQLiteData(conn, log, Json.configure (JsonSerializer.CreateDefault()))
|
||||
|
||||
if hasConnStr "SQLite" then
|
||||
createSQLite (connStr "SQLite")
|
||||
elif hasConnStr "RethinkDB" then
|
||||
let log = sp.GetRequiredService<ILogger<RethinkDbData>> ()
|
||||
let log = sp.GetRequiredService<ILogger<RethinkDbData>>()
|
||||
let _ = Json.configure Converter.Serializer
|
||||
let rethinkCfg = DataConfig.FromUri (connStr "RethinkDB")
|
||||
let conn = await (rethinkCfg.CreateConnectionAsync log)
|
||||
RethinkDbData (conn, rethinkCfg, log)
|
||||
RethinkDbData(conn, rethinkCfg, log)
|
||||
elif hasConnStr "PostgreSQL" then
|
||||
let source = createNpgsqlDataSource config
|
||||
use conn = source.CreateConnection ()
|
||||
let log = sp.GetRequiredService<ILogger<PostgresData>> ()
|
||||
createNpgsqlDataSource config
|
||||
use conn = Postgres.Configuration.dataSource().CreateConnection()
|
||||
let log = sp.GetRequiredService<ILogger<PostgresData>>()
|
||||
log.LogInformation $"Using PostgreSQL database {conn.Database}"
|
||||
PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ()))
|
||||
PostgresData(log, Json.configure (JsonSerializer.CreateDefault()))
|
||||
else
|
||||
createSQLite "Data Source=./myweblog.db;Cache=Shared"
|
||||
if not (Directory.Exists "./data") then Directory.CreateDirectory "./data" |> ignore
|
||||
createSQLite "Data Source=./data/myweblog.db;Cache=Shared"
|
||||
|
||||
|
||||
open System.Threading.Tasks
|
||||
@@ -95,21 +121,21 @@ let showHelp () =
|
||||
printfn "upgrade-user Upgrade a WebLogAdmin user to a full Administrator"
|
||||
printfn " "
|
||||
printfn "For more information on a particular command, run it with no options."
|
||||
Task.FromResult ()
|
||||
Task.FromResult()
|
||||
|
||||
|
||||
open System.IO
|
||||
open BitBadger.AspNetCore.CanonicalDomains
|
||||
open Giraffe
|
||||
open Giraffe.EndpointRouting
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.HttpOverrides
|
||||
open Microsoft.Extensions.Caching.Distributed
|
||||
open NeoSmart.Caching.Sqlite
|
||||
open NeoSmart.Caching.Sqlite.AspNetCore
|
||||
open RethinkDB.DistributedCache
|
||||
|
||||
[<EntryPoint>]
|
||||
let rec main args =
|
||||
let main args =
|
||||
|
||||
let builder = WebApplication.CreateBuilder(args)
|
||||
let _ = builder.Services.Configure<ForwardedHeadersOptions>(fun (opts : ForwardedHeadersOptions) ->
|
||||
@@ -121,16 +147,16 @@ let rec main args =
|
||||
opts.ExpireTimeSpan <- TimeSpan.FromMinutes 60.
|
||||
opts.SlidingExpiration <- true
|
||||
opts.AccessDeniedPath <- "/forbidden")
|
||||
let _ = builder.Services.AddLogging ()
|
||||
let _ = builder.Services.AddAuthorization ()
|
||||
let _ = builder.Services.AddAntiforgery ()
|
||||
let _ = builder.Services.AddLogging()
|
||||
let _ = builder.Services.AddAuthorization()
|
||||
let _ = builder.Services.AddAntiforgery()
|
||||
|
||||
let sp = builder.Services.BuildServiceProvider ()
|
||||
let sp = builder.Services.BuildServiceProvider()
|
||||
let data = DataImplementation.get sp
|
||||
let _ = builder.Services.AddSingleton<JsonSerializer> data.Serializer
|
||||
|
||||
task {
|
||||
do! data.StartUp ()
|
||||
do! data.StartUp()
|
||||
do! WebLogCache.fill data
|
||||
do! ThemeAssetCache.fill data
|
||||
} |> Async.AwaitTask |> Async.RunSynchronously
|
||||
@@ -141,32 +167,26 @@ let rec main args =
|
||||
// A RethinkDB connection is designed to work as a singleton
|
||||
let _ = builder.Services.AddSingleton<IData> data
|
||||
let _ =
|
||||
builder.Services.AddDistributedRethinkDBCache (fun opts ->
|
||||
builder.Services.AddDistributedRethinkDBCache(fun opts ->
|
||||
opts.TableName <- "Session"
|
||||
opts.Connection <- rethink.Conn)
|
||||
()
|
||||
| :? SQLiteData as sql ->
|
||||
| :? SQLiteData ->
|
||||
// ADO.NET connections are designed to work as per-request instantiation
|
||||
let cfg = sp.GetRequiredService<IConfiguration> ()
|
||||
let _ =
|
||||
builder.Services.AddScoped<SqliteConnection> (fun sp ->
|
||||
let conn = new SqliteConnection (sql.Conn.ConnectionString)
|
||||
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
|
||||
conn)
|
||||
let _ = builder.Services.AddScoped<IData, SQLiteData> () |> ignore
|
||||
let cfg = sp.GetRequiredService<IConfiguration>()
|
||||
let _ = builder.Services.AddScoped<SqliteConnection>(fun sp -> Sqlite.Configuration.dbConn ())
|
||||
let _ = builder.Services.AddScoped<IData, SQLiteData>()
|
||||
// Use SQLite for caching as well
|
||||
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"
|
||||
let _ = builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath)
|
||||
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./data/session.db"
|
||||
let _ = builder.Services.AddSqliteCache(fun o -> o.CachePath <- cachePath)
|
||||
()
|
||||
| :? PostgresData as postgres ->
|
||||
// ADO.NET Data Sources are designed to work as singletons
|
||||
let _ =
|
||||
builder.Services.AddSingleton<NpgsqlDataSource> (fun sp ->
|
||||
DataImplementation.createNpgsqlDataSource (sp.GetRequiredService<IConfiguration> ()))
|
||||
let _ = builder.Services.AddSingleton<NpgsqlDataSource>(Postgres.Configuration.dataSource ())
|
||||
let _ = builder.Services.AddSingleton<IData> postgres
|
||||
let _ =
|
||||
builder.Services.AddSingleton<IDistributedCache> (fun _ ->
|
||||
Postgres.DistributedCache () :> IDistributedCache)
|
||||
builder.Services.AddSingleton<IDistributedCache>(fun _ ->
|
||||
Postgres.DistributedCache() :> IDistributedCache)
|
||||
()
|
||||
| _ -> ()
|
||||
|
||||
@@ -174,12 +194,12 @@ let rec main args =
|
||||
opts.IdleTimeout <- TimeSpan.FromMinutes 60
|
||||
opts.Cookie.HttpOnly <- true
|
||||
opts.Cookie.IsEssential <- true)
|
||||
let _ = builder.Services.AddGiraffe ()
|
||||
let _ = builder.Services.AddGiraffe()
|
||||
|
||||
// Set up DotLiquid
|
||||
DotLiquidBespoke.register ()
|
||||
|
||||
let app = builder.Build ()
|
||||
let app = builder.Build()
|
||||
|
||||
match args |> Array.tryHead with
|
||||
| Some it when it = "init" -> Maintenance.createWebLog args app.Services
|
||||
@@ -195,20 +215,29 @@ let rec main args =
|
||||
printfn $"""Unrecognized command "{it}" - valid commands are:"""
|
||||
showHelp ()
|
||||
| None -> task {
|
||||
// Load all themes in the application directory
|
||||
for themeFile in Directory.EnumerateFiles (".", "*-theme.zip") do
|
||||
do! Maintenance.loadTheme [| ""; themeFile |] app.Services
|
||||
// Load admin and default themes, and all themes in the /themes directory
|
||||
do! Maintenance.loadTheme [| ""; "./admin-theme.zip" |] app.Services
|
||||
do! Maintenance.loadTheme [| ""; "./default-theme.zip" |] app.Services
|
||||
if Directory.Exists "./themes" then
|
||||
for themeFile in Directory.EnumerateFiles("./themes", "*-theme.zip") do
|
||||
do! Maintenance.loadTheme [| ""; themeFile |] app.Services
|
||||
|
||||
let _ = app.UseForwardedHeaders ()
|
||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
||||
let _ = app.UseAuthentication ()
|
||||
let _ = app.UseStaticFiles ()
|
||||
let _ = app.UseRouting ()
|
||||
let _ = app.UseSession ()
|
||||
let _ = app.UseForwardedHeaders()
|
||||
|
||||
(app.Services.GetRequiredService<IConfiguration>().GetSection "CanonicalDomains").Value
|
||||
|> (isNull >> not)
|
||||
|> function true -> app.UseCanonicalDomains() |> ignore | false -> ()
|
||||
|
||||
let _ = app.UseCookiePolicy(CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
let _ = app.UseMiddleware<WebLogMiddleware>()
|
||||
let _ = app.UseMiddleware<RedirectRuleMiddleware>()
|
||||
let _ = app.UseAuthentication()
|
||||
let _ = app.UseStaticFiles()
|
||||
let _ = app.UseRouting()
|
||||
let _ = app.UseSession()
|
||||
let _ = app.UseGiraffe Handlers.Routes.endpoint
|
||||
|
||||
app.Run ()
|
||||
app.Run()
|
||||
}
|
||||
|> Async.AwaitTask |> Async.RunSynchronously
|
||||
|
||||
|
||||
Reference in New Issue
Block a user