diff --git a/src/MyWebLog.Data/Interfaces.fs b/src/MyWebLog.Data/Interfaces.fs index 0129045..c0af2f9 100644 --- a/src/MyWebLog.Data/Interfaces.fs +++ b/src/MyWebLog.Data/Interfaces.fs @@ -208,9 +208,12 @@ type IUploadData = /// Find an uploaded file by its path for the given web log abstract member findByPath : string -> WebLogId -> Task - /// Find all uploaded files for a web log + /// Find all uploaded files for a web log (excludes data) abstract member findByWebLog : WebLogId -> Task + /// Find all uploaded files for a web log + abstract member findByWebLogWithData : WebLogId -> Task + /// Restore uploaded files from a backup abstract member restore : Upload list -> Task diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index f392c37..af706e9 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -755,6 +755,14 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger tryFirst <| conn member _.findByWebLog webLogId = rethink { + 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 { withTable Table.Upload between (r.Array (webLogId, r.Minval ())) (r.Array (webLogId, r.Maxval ())) [ Index "webLogAndPath" ] diff --git a/src/MyWebLog.Data/SQLite/Helpers.fs b/src/MyWebLog.Data/SQLite/Helpers.fs index 4321826..22c5b77 100644 --- a/src/MyWebLog.Data/SQLite/Helpers.fs +++ b/src/MyWebLog.Data/SQLite/Helpers.fs @@ -249,16 +249,20 @@ module Map = } /// 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) webLogId = WebLogId (getString "web_log_id" rdr) path = Permalink (getString "path" rdr) updatedOn = getDateTime "updated_on" rdr - data = - use dataStream = new MemoryStream () - use blobStream = getStream "data" rdr - blobStream.CopyTo dataStream - dataStream.ToArray () + data = data } /// Create a web log from the current row in the given data reader diff --git a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs index beb275c..e2d8b5c 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs @@ -44,16 +44,25 @@ type SQLiteUploadData (conn : SqliteConnection) = addWebLogId cmd webLogId cmd.Parameters.AddWithValue ("@path", path) |> ignore 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 - let findByWebLog webLogId = backgroundTask { + let findByWebLogWithData webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- "SELECT *, ROWID FROM upload WHERE web_log_id = @webLogId" addWebLogId cmd webLogId let! rdr = cmd.ExecuteReaderAsync () - return toList Map.toUpload rdr + return toList (Map.toUpload true) rdr } /// Restore uploads from a backup @@ -65,5 +74,6 @@ type SQLiteUploadData (conn : SqliteConnection) = member _.add upload = add upload member _.findByPath path webLogId = findByPath path webLogId member _.findByWebLog webLogId = findByWebLog webLogId + member _.findByWebLogWithData webLogId = findByWebLogWithData webLogId member _.restore uploads = restore uploads \ No newline at end of file diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 5e336c8..2b1d836 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -12,6 +12,29 @@ module private Helpers = match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed +/// The model used to display the admin dashboard +[] +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 [] type DisplayCategory = @@ -124,27 +147,37 @@ type DisplayPage = } -/// The model used to display the admin dashboard +open System.IO + +/// Information about an uploaded file used for display [] -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 +type DisplayUpload = + { /// The ID of the uploaded file + id : string + + /// The name of the uploaded file + name : string + + /// The path at which the file is served + path : string + + /// The date/time the file was updated + updatedOn : DateTime option + + /// The source for this file (created from UploadDestination DU) + source : string } + + /// 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 diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index c74bb9d..e644c72 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -226,11 +226,11 @@ let register () = typeof; typeof; typeof; typeof; typeof; typeof typeof; typeof // View models - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof; typeof; typeof; typeof - typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof; typeof; typeof + typeof; typeof // Framework types typeof; typeof; typeof; typeof typeof; typeof; typeof diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 49cab08..985afaa 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -143,6 +143,9 @@ let router : HttpHandler = choose [ ]) ]) route "/theme/update" >=> Admin.themeUpdatePage + subRoute "/upload" (choose [ + route "s" >=> Upload.list + ]) route "/user/edit" >=> User.edit ] POST >=> validateCsrf >=> choose [ diff --git a/src/MyWebLog/Handlers/Upload.fs b/src/MyWebLog/Handlers/Upload.fs index 36f196b..1ce16e1 100644 --- a/src/MyWebLog/Handlers/Upload.fs +++ b/src/MyWebLog/Handlers/Upload.fs @@ -59,3 +59,52 @@ let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task { else 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 + } \ No newline at end of file diff --git a/src/MyWebLog/Maintenance.fs b/src/MyWebLog/Maintenance.fs index 1f4502c..8504e3f 100644 --- a/src/MyWebLog/Maintenance.fs +++ b/src/MyWebLog/Maintenance.fs @@ -292,7 +292,7 @@ module Backup = let! posts = data.Post.findFullByWebLog webLog.id printfn "- Exporting uploads..." - let! uploads = data.Upload.findByWebLog webLog.id + let! uploads = data.Upload.findByWebLogWithData webLog.id printfn "- Writing archive..." let archive = { diff --git a/src/admin-theme/_layout.liquid b/src/admin-theme/_layout.liquid index 46737cb..856af96 100644 --- a/src/admin-theme/_layout.liquid +++ b/src/admin-theme/_layout.liquid @@ -12,6 +12,7 @@ {{ "admin/dashboard" | nav_link: "Dashboard" }} {{ "admin/pages" | nav_link: "Pages" }} {{ "admin/posts" | nav_link: "Posts" }} + {{ "admin/uploads" | nav_link: "Uploads" }} {{ "admin/categories" | nav_link: "Categories" }} {{ "admin/settings" | nav_link: "Settings" }} diff --git a/src/admin-theme/upload-list.liquid b/src/admin-theme/upload-list.liquid new file mode 100644 index 0000000..7f8e813 --- /dev/null +++ b/src/admin-theme/upload-list.liquid @@ -0,0 +1,32 @@ +

{{ page_title }}

+
+ Upload a New File +
+ +
+
File Name
+
Path
+
File Date/Time
+
+ {%- assign file_count = files | size -%} + {%- if file_count > 0 %} + {% for file in files %} +
+
{{ file.name }}
+
+ + {{ file.source }} + {{ file.path }} +
+
+ {% if file.updated_on %}{{ file.updated_on.value | date: "yyyy-MM-dd/HH:mm" }}{% else %}--{% endif %} +
+
+ {% endfor %} + {%- else -%} +
+
This web log has uploaded files
+
+ {%- endif %} +
+