Add copy links to upload list (#2)

This commit is contained in:
Daniel J. Summers 2022-06-30 18:56:24 -04:00
parent 0567dff54a
commit feada6f11f
4 changed files with 84 additions and 34 deletions

View File

@ -4,6 +4,7 @@ module MyWebLog.Handlers.Upload
open System open System
open Giraffe open Giraffe
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Microsoft.Net.Http.Headers
open MyWebLog open MyWebLog
/// Helper functions for this module /// Helper functions for this module
@ -15,6 +16,12 @@ module private Helpers =
/// A MIME type mapper instance to use when serving files from the database /// A MIME type mapper instance to use when serving files from the database
let mimeMap = FileExtensionContentTypeProvider () let mimeMap = FileExtensionContentTypeProvider ()
/// A cache control header that instructs the browser to cache the result for no more than 30 days
let cacheForThirtyDays =
let hdr = CacheControlHeaderValue()
hdr.MaxAge <- Some (TimeSpan.FromDays 30) |> Option.toNullable
hdr
/// Determine if the file has been modified since the date/time specified by the If-Modified-Since header /// Determine if the file has been modified since the date/time specified by the If-Modified-Since header
let checkModified since (ctx : HttpContext) : HttpHandler option = let checkModified since (ctx : HttpContext) : HttpHandler option =
@ -24,55 +31,55 @@ let checkModified since (ctx : HttpContext) : HttpHandler option =
| _ -> Some (setStatusCode 304 >=> setBodyFromString "Not Modified") | _ -> Some (setStatusCode 304 >=> setBodyFromString "Not Modified")
open System.IO
open Microsoft.AspNetCore.Http.Headers open Microsoft.AspNetCore.Http.Headers
open Microsoft.Net.Http.Headers
/// Derive a MIME type based on the extension of the file /// Derive a MIME type based on the extension of the file
let deriveMimeType path = let deriveMimeType path =
match mimeMap.TryGetContentType path with true, typ -> typ | false, _ -> "application/octet-stream" match mimeMap.TryGetContentType path with true, typ -> typ | false, _ -> "application/octet-stream"
/// Send a file, caching the response for 30 days /// Send a file, caching the response for 30 days
let sendFile updatedOn path data : HttpHandler = fun next ctx -> task { let sendFile updatedOn path (data : byte[]) : HttpHandler = fun next ctx -> task {
let headers = ResponseHeaders ctx.Response.Headers let headers = ResponseHeaders ctx.Response.Headers
headers.LastModified <- Some (DateTimeOffset updatedOn) |> Option.toNullable
headers.ContentType <- (deriveMimeType >> MediaTypeHeaderValue) path headers.ContentType <- (deriveMimeType >> MediaTypeHeaderValue) path
headers.CacheControl <- headers.CacheControl <- cacheForThirtyDays
let hdr = CacheControlHeaderValue() let stream = new MemoryStream (data)
hdr.MaxAge <- Some (TimeSpan.FromDays 30) |> Option.toNullable return! streamData true stream None (Some (DateTimeOffset updatedOn)) next ctx
hdr
return! setBody data next ctx
} }
// GET /upload/{web-log-slug}/{**path} // GET /upload/{web-log-slug}/{**path}
let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task { let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
let webLog = ctx.WebLog
let parts = (urlParts |> Seq.skip 1 |> Seq.head).Split '/' let parts = (urlParts |> Seq.skip 1 |> Seq.head).Split '/'
let slug = Array.head parts let slug = Array.head parts
let path = String.Join ('/', parts |> Array.skip 1)
let webLog = ctx.WebLog
if slug = webLog.slug then if slug = webLog.slug then
match! ctx.Data.Upload.findByPath path webLog.id with // Static file middleware will not work in subdirectories; check for an actual file first
| Some upload -> let fileName = Path.Combine ("wwwroot", (Seq.head urlParts)[1..])
match checkModified upload.updatedOn ctx with if File.Exists fileName then
| Some threeOhFour -> return! threeOhFour next ctx return! streamFile true fileName None None next ctx
| None -> return! sendFile upload.updatedOn path upload.data next ctx else
| None -> return! Error.notFound next ctx let path = String.Join ('/', Array.skip 1 parts)
match! ctx.Data.Upload.findByPath path webLog.id with
| Some upload ->
match checkModified upload.updatedOn ctx with
| Some threeOhFour -> return! threeOhFour next ctx
| None -> return! sendFile upload.updatedOn path upload.data next ctx
| None -> return! Error.notFound next ctx
else else
return! Error.notFound next ctx return! Error.notFound next ctx
} }
// ADMIN // ADMIN
open System.IO
open DotLiquid open DotLiquid
open MyWebLog.ViewModels open MyWebLog.ViewModels
// GET /admin/uploads // GET /admin/uploads
let list : HttpHandler = fun next ctx -> task { let list : HttpHandler = fun next ctx -> task {
let webLog = ctx.WebLog let webLog = ctx.WebLog
let! dbUploads = ctx.Data.Upload.findByWebLog webLog.id let! dbUploads = ctx.Data.Upload.findByWebLog webLog.id
let diskUploads = let diskUploads =
let path = Path.Combine ("wwwroot", "upload", webLog.slug) let path = Path.Combine ("wwwroot", "upload", webLog.slug)
printfn $"Files in %s{path}"
try try
Directory.EnumerateFiles (path, "*", SearchOption.AllDirectories) Directory.EnumerateFiles (path, "*", SearchOption.AllDirectories)
|> Seq.map (fun file -> |> Seq.map (fun file ->
@ -83,7 +90,7 @@ let list : HttpHandler = fun next ctx -> task {
| _ -> None | _ -> None
{ DisplayUpload.id = "" { DisplayUpload.id = ""
name = name name = name
path = file.Substring(8).Replace (name, "") path = file.Replace($"{path}/", "").Replace (name, "")
updatedOn = create updatedOn = create
source = UploadDestination.toString Disk source = UploadDestination.toString Disk
}) })
@ -93,7 +100,6 @@ let list : HttpHandler = fun next ctx -> task {
| ex -> | ex ->
warn "Upload" ctx $"Encountered {ex.GetType().Name} listing uploads for {path}:\n{ex.Message}" warn "Upload" ctx $"Encountered {ex.GetType().Name} listing uploads for {path}:\n{ex.Message}"
[] []
printfn "done"
let allFiles = let allFiles =
dbUploads dbUploads
|> List.map (DisplayUpload.fromUpload Database) |> List.map (DisplayUpload.fromUpload Database)
@ -107,4 +113,4 @@ let list : HttpHandler = fun next ctx -> task {
files = allFiles files = allFiles
|} |}
|> viewForTheme "admin" "upload-list" next ctx |> viewForTheme "admin" "upload-list" next ctx
} }

View File

@ -42,7 +42,7 @@
</span> </span>
</div> </div>
<div class="{{ title_col }}"> <div class="{{ title_col }}">
{%- if post.episode %}<span class="badge bg-success float-end text-uppercase">Episode</span>{% endif -%} {%- if post.episode %}<span class="badge bg-success float-end text-uppercase mt-1">Episode</span>{% endif -%}
{{ post.title }}<br> {{ post.title }}<br>
<small> <small>
<a href="{{ post | relative_link }}" target="_blank">View Post</a> <a href="{{ post | relative_link }}" target="_blank">View Post</a>

View File

@ -1,24 +1,56 @@
<h2 class="my-3">{{ page_title }}</h2> <h2 class="my-3">{{ page_title }}</h2>
<article> <article>
{%- capture base_url %}{{ "" | relative_link }}{% endcapture -%}
{%- capture upload_path %}upload/{{ web_log.slug }}/{% endcapture -%}
{%- capture upload_base %}{{ base_url }}{{ upload_path }}{% endcapture -%}
<a href="{{ "admin/upload/new" | relative_link }}" class="btn btn-primary btn-sm mb-3">Upload a New File</a> <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"> <form method="post" class="container" hx-target="body">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row">
<div class="col text-muted text-center"><em>Uploaded files served from</em><br>{{ upload_base }}</div>
</div>
<div class="row mwl-table-heading"> <div class="row mwl-table-heading">
<div class="col">File Name</div> <div class="col-6">File Name</div>
<div class="col">Path</div> <div class="col-3">Path</div>
<div class="col">File Date/Time</div> <div class="col-3">File Date/Time</div>
</div> </div>
{%- assign file_count = files | size -%} {%- assign file_count = files | size -%}
{%- if file_count > 0 %} {%- if file_count > 0 %}
{% for file in files %} {% for file in files %}
<div class="row mwl-table-detail"> <div class="row mwl-table-detail">
<div class="col">{{ file.name }}</div> <div class="col-6">
<div class="col"> {%- capture badge_class -%}
<span class="badge bg-{% if file.source == "disk" %}secondary{% else %}primary{% endif %} text-uppercase"> {%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
{{ file.source }} {%- endcapture -%}
</span> {{ file.path }} {%- capture rel_url %}{{ upload_base }}{{ file.path }}{{ file.name }}{% endcapture -%}
{%- capture blog_rel %}{{ upload_path }}{{ file.path }}{{ file.name }}{% endcapture -%}
<span class="badge bg-{{ badge_class }} text-uppercase float-end mt-1">{{ file.source }}</span>
{{ file.name }}<br>
<small>
<a href="{{ rel_url }}" target="_blank">View File</a>
<span class="text-muted"> &bull; Copy </span>
<a href="{{ blog_rel | absolute_link }}" hx-boost="false"
onclick="return Admin.copyText('{{ blog_rel | absolute_link }}', this)">
Absolute
</a>
<span class="text-muted"> | </span>
<a href="{{ blog_rel | relative_link }}" hx-boost="false"
onclick="return Admin.copyText('{{ blog_rel | relative_link }}', this)">
Relative
</a>
{%- unless base_url == "/" %}
<span class="text-muted"> | </span>
<a href="{{ blog_rel }}" hx-boost="false"
onclick="return Admin.copyText('/{{ blog_rel }}', this)">
For Post
</a>
{%- endunless %}
<span class="text-muted"> Link &bull; </span>
<a href="#" onclick="" class="text-danger">Delete</a>
</small>
</div> </div>
<div class="col"> <div class="col-3">{{ file.path }}</div>
<div class="col-3">
{% if file.updated_on %}{{ file.updated_on.value | date: "yyyy-MM-dd/HH:mm" }}{% else %}--{% endif %} {% if file.updated_on %}{{ file.updated_on.value | date: "yyyy-MM-dd/HH:mm" }}{% else %}--{% endif %}
</div> </div>
</div> </div>

View File

@ -220,6 +220,18 @@
checkPodcast() { checkPodcast() {
document.getElementById("podcastFields").disabled = !document.getElementById("isPodcast").checked document.getElementById("podcastFields").disabled = !document.getElementById("isPodcast").checked
}, },
/**
* Copy text to the clipboard
* @param text {string} The text to be copied
* @param elt {HTMLAnchorElement} The element on which the click was generated
* @return {boolean} False, to prevent navigation
*/
copyText(text, elt) {
navigator.clipboard.writeText(text)
elt.innerText = "Copied"
return false
},
/** /**
* Toggle the source of a custom RSS feed * Toggle the source of a custom RSS feed