WIP on upload admin (#2)
This commit is contained in:
parent
c29bbc04ac
commit
0567dff54a
|
@ -208,9 +208,12 @@ type IUploadData =
|
||||||
/// Find an uploaded file by its path for the given web log
|
/// Find an uploaded file by its path for the given web log
|
||||||
abstract member findByPath : string -> WebLogId -> Task<Upload option>
|
abstract member findByPath : string -> WebLogId -> Task<Upload option>
|
||||||
|
|
||||||
/// Find all uploaded files for a web log
|
/// Find all uploaded files for a web log (excludes data)
|
||||||
abstract member findByWebLog : WebLogId -> Task<Upload list>
|
abstract member findByWebLog : WebLogId -> Task<Upload list>
|
||||||
|
|
||||||
|
/// Find all uploaded files for a web log
|
||||||
|
abstract member findByWebLogWithData : WebLogId -> Task<Upload list>
|
||||||
|
|
||||||
/// Restore uploaded files from a backup
|
/// Restore uploaded files from a backup
|
||||||
abstract member restore : Upload list -> Task<unit>
|
abstract member restore : Upload list -> Task<unit>
|
||||||
|
|
||||||
|
|
|
@ -755,6 +755,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
||||||
|> tryFirst <| conn
|
|> tryFirst <| conn
|
||||||
|
|
||||||
member _.findByWebLog webLogId = rethink<Upload> {
|
member _.findByWebLog webLogId = rethink<Upload> {
|
||||||
|
withTable Table.Upload
|
||||||
|
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ()))
|
||||||
|
[ Index "webLogAndPath" ]
|
||||||
|
without [ "data" ]
|
||||||
|
resultCursor; withRetryCursorDefault; toList conn
|
||||||
|
}
|
||||||
|
|
||||||
|
member _.findByWebLogWithData webLogId = rethink<Upload> {
|
||||||
withTable Table.Upload
|
withTable Table.Upload
|
||||||
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ()))
|
between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ()))
|
||||||
[ Index "webLogAndPath" ]
|
[ Index "webLogAndPath" ]
|
||||||
|
|
|
@ -249,16 +249,20 @@ module Map =
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an uploaded file from the current row in the given data reader
|
/// Create an uploaded file from the current row in the given data reader
|
||||||
let toUpload (rdr : SqliteDataReader) : Upload =
|
let toUpload includeData (rdr : SqliteDataReader) : Upload =
|
||||||
|
let data =
|
||||||
|
if includeData then
|
||||||
|
use dataStream = new MemoryStream ()
|
||||||
|
use blobStream = getStream "data" rdr
|
||||||
|
blobStream.CopyTo dataStream
|
||||||
|
dataStream.ToArray ()
|
||||||
|
else
|
||||||
|
[||]
|
||||||
{ id = UploadId (getString "id" rdr)
|
{ id = UploadId (getString "id" rdr)
|
||||||
webLogId = WebLogId (getString "web_log_id" rdr)
|
webLogId = WebLogId (getString "web_log_id" rdr)
|
||||||
path = Permalink (getString "path" rdr)
|
path = Permalink (getString "path" rdr)
|
||||||
updatedOn = getDateTime "updated_on" rdr
|
updatedOn = getDateTime "updated_on" rdr
|
||||||
data =
|
data = data
|
||||||
use dataStream = new MemoryStream ()
|
|
||||||
use blobStream = getStream "data" rdr
|
|
||||||
blobStream.CopyTo dataStream
|
|
||||||
dataStream.ToArray ()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a web log from the current row in the given data reader
|
/// Create a web log from the current row in the given data reader
|
||||||
|
|
|
@ -44,16 +44,25 @@ type SQLiteUploadData (conn : SqliteConnection) =
|
||||||
addWebLogId cmd webLogId
|
addWebLogId cmd webLogId
|
||||||
cmd.Parameters.AddWithValue ("@path", path) |> ignore
|
cmd.Parameters.AddWithValue ("@path", path) |> ignore
|
||||||
let! rdr = cmd.ExecuteReaderAsync ()
|
let! rdr = cmd.ExecuteReaderAsync ()
|
||||||
return if rdr.Read () then Some (Map.toUpload rdr) else None
|
return if rdr.Read () then Some (Map.toUpload true rdr) else None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all uploaded files for the given web log (excludes data)
|
||||||
|
let findByWebLog webLogId = backgroundTask {
|
||||||
|
use cmd = conn.CreateCommand ()
|
||||||
|
cmd.CommandText <- "SELECT id, web_log_id, path, updated_on FROM upload WHERE web_log_id = @webLogId"
|
||||||
|
addWebLogId cmd webLogId
|
||||||
|
let! rdr = cmd.ExecuteReaderAsync ()
|
||||||
|
return toList (Map.toUpload false) rdr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find all uploaded files for the given web log
|
/// Find all uploaded files for the given web log
|
||||||
let findByWebLog webLogId = backgroundTask {
|
let findByWebLogWithData webLogId = backgroundTask {
|
||||||
use cmd = conn.CreateCommand ()
|
use cmd = conn.CreateCommand ()
|
||||||
cmd.CommandText <- "SELECT *, ROWID FROM upload WHERE web_log_id = @webLogId"
|
cmd.CommandText <- "SELECT *, ROWID FROM upload WHERE web_log_id = @webLogId"
|
||||||
addWebLogId cmd webLogId
|
addWebLogId cmd webLogId
|
||||||
let! rdr = cmd.ExecuteReaderAsync ()
|
let! rdr = cmd.ExecuteReaderAsync ()
|
||||||
return toList Map.toUpload rdr
|
return toList (Map.toUpload true) rdr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore uploads from a backup
|
/// Restore uploads from a backup
|
||||||
|
@ -65,5 +74,6 @@ type SQLiteUploadData (conn : SqliteConnection) =
|
||||||
member _.add upload = add upload
|
member _.add upload = add upload
|
||||||
member _.findByPath path webLogId = findByPath path webLogId
|
member _.findByPath path webLogId = findByPath path webLogId
|
||||||
member _.findByWebLog webLogId = findByWebLog webLogId
|
member _.findByWebLog webLogId = findByWebLog webLogId
|
||||||
|
member _.findByWebLogWithData webLogId = findByWebLogWithData webLogId
|
||||||
member _.restore uploads = restore uploads
|
member _.restore uploads = restore uploads
|
||||||
|
|
|
@ -12,6 +12,29 @@ module private Helpers =
|
||||||
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
||||||
|
|
||||||
|
|
||||||
|
/// The model used to display the admin dashboard
|
||||||
|
[<NoComparison; NoEquality>]
|
||||||
|
type DashboardModel =
|
||||||
|
{ /// The number of published posts
|
||||||
|
posts : int
|
||||||
|
|
||||||
|
/// The number of post drafts
|
||||||
|
drafts : int
|
||||||
|
|
||||||
|
/// The number of pages
|
||||||
|
pages : int
|
||||||
|
|
||||||
|
/// The number of pages in the page list
|
||||||
|
listedPages : int
|
||||||
|
|
||||||
|
/// The number of categories
|
||||||
|
categories : int
|
||||||
|
|
||||||
|
/// The top-level categories
|
||||||
|
topLevelCategories : int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Details about a category, used to display category lists
|
/// Details about a category, used to display category lists
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DisplayCategory =
|
type DisplayCategory =
|
||||||
|
@ -124,28 +147,38 @@ type DisplayPage =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The model used to display the admin dashboard
|
open System.IO
|
||||||
|
|
||||||
|
/// Information about an uploaded file used for display
|
||||||
[<NoComparison; NoEquality>]
|
[<NoComparison; NoEquality>]
|
||||||
type DashboardModel =
|
type DisplayUpload =
|
||||||
{ /// The number of published posts
|
{ /// The ID of the uploaded file
|
||||||
posts : int
|
id : string
|
||||||
|
|
||||||
/// The number of post drafts
|
/// The name of the uploaded file
|
||||||
drafts : int
|
name : string
|
||||||
|
|
||||||
/// The number of pages
|
/// The path at which the file is served
|
||||||
pages : int
|
path : string
|
||||||
|
|
||||||
/// The number of pages in the page list
|
/// The date/time the file was updated
|
||||||
listedPages : int
|
updatedOn : DateTime option
|
||||||
|
|
||||||
/// The number of categories
|
/// The source for this file (created from UploadDestination DU)
|
||||||
categories : int
|
source : string
|
||||||
|
|
||||||
/// The top-level categories
|
|
||||||
topLevelCategories : int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a display uploaded file
|
||||||
|
static member fromUpload source (upload : Upload) =
|
||||||
|
let path = Permalink.toString upload.path
|
||||||
|
let name = Path.GetFileName path
|
||||||
|
{ id = UploadId.toString upload.id
|
||||||
|
name = name
|
||||||
|
path = path.Replace (name, "")
|
||||||
|
updatedOn = Some upload.updatedOn
|
||||||
|
source = UploadDestination.toString source
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// View model for editing categories
|
/// View model for editing categories
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
|
|
@ -226,11 +226,11 @@ let register () =
|
||||||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>
|
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>
|
||||||
typeof<TagMap>; typeof<WebLog>
|
typeof<TagMap>; typeof<WebLog>
|
||||||
// View models
|
// View models
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
typeof<DisplayUpload>; typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>
|
||||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
typeof<EditPostModel>; typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
||||||
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
typeof<LogOnModel>; typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>
|
||||||
typeof<UserMessage>
|
typeof<SettingsModel>; typeof<UserMessage>
|
||||||
// Framework types
|
// Framework types
|
||||||
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
||||||
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
|
|
@ -143,6 +143,9 @@ let router : HttpHandler = choose [
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
route "/theme/update" >=> Admin.themeUpdatePage
|
route "/theme/update" >=> Admin.themeUpdatePage
|
||||||
|
subRoute "/upload" (choose [
|
||||||
|
route "s" >=> Upload.list
|
||||||
|
])
|
||||||
route "/user/edit" >=> User.edit
|
route "/user/edit" >=> User.edit
|
||||||
]
|
]
|
||||||
POST >=> validateCsrf >=> choose [
|
POST >=> validateCsrf >=> choose [
|
||||||
|
|
|
@ -59,3 +59,52 @@ let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
|
||||||
else
|
else
|
||||||
return! Error.notFound next ctx
|
return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADMIN
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open DotLiquid
|
||||||
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
|
// GET /admin/uploads
|
||||||
|
let list : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
let! dbUploads = ctx.Data.Upload.findByWebLog webLog.id
|
||||||
|
let diskUploads =
|
||||||
|
let path = Path.Combine ("wwwroot", "upload", webLog.slug)
|
||||||
|
printfn $"Files in %s{path}"
|
||||||
|
try
|
||||||
|
Directory.EnumerateFiles (path, "*", SearchOption.AllDirectories)
|
||||||
|
|> Seq.map (fun file ->
|
||||||
|
let name = Path.GetFileName file
|
||||||
|
let create =
|
||||||
|
match File.GetCreationTime (Path.Combine (path, file)) with
|
||||||
|
| dt when dt > DateTime.UnixEpoch -> Some dt
|
||||||
|
| _ -> None
|
||||||
|
{ DisplayUpload.id = ""
|
||||||
|
name = name
|
||||||
|
path = file.Substring(8).Replace (name, "")
|
||||||
|
updatedOn = create
|
||||||
|
source = UploadDestination.toString Disk
|
||||||
|
})
|
||||||
|
|> List.ofSeq
|
||||||
|
with
|
||||||
|
| :? DirectoryNotFoundException -> [] // This is fine
|
||||||
|
| ex ->
|
||||||
|
warn "Upload" ctx $"Encountered {ex.GetType().Name} listing uploads for {path}:\n{ex.Message}"
|
||||||
|
[]
|
||||||
|
printfn "done"
|
||||||
|
let allFiles =
|
||||||
|
dbUploads
|
||||||
|
|> List.map (DisplayUpload.fromUpload Database)
|
||||||
|
|> List.append diskUploads
|
||||||
|
|> List.sortByDescending (fun file -> file.updatedOn, file.path)
|
||||||
|
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
csrf = csrfToken ctx
|
||||||
|
page_title = "Uploaded Files"
|
||||||
|
files = allFiles
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "upload-list" next ctx
|
||||||
|
}
|
|
@ -292,7 +292,7 @@ module Backup =
|
||||||
let! posts = data.Post.findFullByWebLog webLog.id
|
let! posts = data.Post.findFullByWebLog webLog.id
|
||||||
|
|
||||||
printfn "- Exporting uploads..."
|
printfn "- Exporting uploads..."
|
||||||
let! uploads = data.Upload.findByWebLog webLog.id
|
let! uploads = data.Upload.findByWebLogWithData webLog.id
|
||||||
|
|
||||||
printfn "- Writing archive..."
|
printfn "- Writing archive..."
|
||||||
let archive = {
|
let archive = {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
||||||
{{ "admin/pages" | nav_link: "Pages" }}
|
{{ "admin/pages" | nav_link: "Pages" }}
|
||||||
{{ "admin/posts" | nav_link: "Posts" }}
|
{{ "admin/posts" | nav_link: "Posts" }}
|
||||||
|
{{ "admin/uploads" | nav_link: "Uploads" }}
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
32
src/admin-theme/upload-list.liquid
Normal file
32
src/admin-theme/upload-list.liquid
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<a href="{{ "admin/upload/new" | relative_link }}" class="btn btn-primary btn-sm mb-3">Upload a New File</a>
|
||||||
|
<form method="post" class="container" hx-target="body">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class="col">File Name</div>
|
||||||
|
<div class="col">Path</div>
|
||||||
|
<div class="col">File Date/Time</div>
|
||||||
|
</div>
|
||||||
|
{%- assign file_count = files | size -%}
|
||||||
|
{%- if file_count > 0 %}
|
||||||
|
{% for file in files %}
|
||||||
|
<div class="row mwl-table-detail">
|
||||||
|
<div class="col">{{ file.name }}</div>
|
||||||
|
<div class="col">
|
||||||
|
<span class="badge bg-{% if file.source == "disk" %}secondary{% else %}primary{% endif %} text-uppercase">
|
||||||
|
{{ file.source }}
|
||||||
|
</span> {{ file.path }}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{% if file.updated_on %}{{ file.updated_on.value | date: "yyyy-MM-dd/HH:mm" }}{% else %}--{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{%- else -%}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-muted fst-italic text-center">This web log has uploaded files</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
</form>
|
||||||
|
</article>
|
Loading…
Reference in New Issue
Block a user