Add copy links to upload list (#2)
This commit is contained in:
parent
0567dff54a
commit
feada6f11f
@ -4,6 +4,7 @@ module MyWebLog.Handlers.Upload
|
||||
open System
|
||||
open Giraffe
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Net.Http.Headers
|
||||
open MyWebLog
|
||||
|
||||
/// 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
|
||||
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
|
||||
let checkModified since (ctx : HttpContext) : HttpHandler option =
|
||||
@ -24,55 +31,55 @@ let checkModified since (ctx : HttpContext) : HttpHandler option =
|
||||
| _ -> Some (setStatusCode 304 >=> setBodyFromString "Not Modified")
|
||||
|
||||
|
||||
open System.IO
|
||||
open Microsoft.AspNetCore.Http.Headers
|
||||
open Microsoft.Net.Http.Headers
|
||||
|
||||
/// Derive a MIME type based on the extension of the file
|
||||
let deriveMimeType path =
|
||||
match mimeMap.TryGetContentType path with true, typ -> typ | false, _ -> "application/octet-stream"
|
||||
|
||||
/// 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
|
||||
headers.LastModified <- Some (DateTimeOffset updatedOn) |> Option.toNullable
|
||||
headers.ContentType <- (deriveMimeType >> MediaTypeHeaderValue) path
|
||||
headers.CacheControl <-
|
||||
let hdr = CacheControlHeaderValue()
|
||||
hdr.MaxAge <- Some (TimeSpan.FromDays 30) |> Option.toNullable
|
||||
hdr
|
||||
return! setBody data next ctx
|
||||
headers.CacheControl <- cacheForThirtyDays
|
||||
let stream = new MemoryStream (data)
|
||||
return! streamData true stream None (Some (DateTimeOffset updatedOn)) next ctx
|
||||
}
|
||||
|
||||
// GET /upload/{web-log-slug}/{**path}
|
||||
let serve (urlParts : string seq) : HttpHandler = fun next ctx -> task {
|
||||
let webLog = ctx.WebLog
|
||||
let parts = (urlParts |> Seq.skip 1 |> Seq.head).Split '/'
|
||||
let slug = Array.head parts
|
||||
let path = String.Join ('/', parts |> Array.skip 1)
|
||||
let webLog = ctx.WebLog
|
||||
if slug = webLog.slug then
|
||||
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
|
||||
// Static file middleware will not work in subdirectories; check for an actual file first
|
||||
let fileName = Path.Combine ("wwwroot", (Seq.head urlParts)[1..])
|
||||
if File.Exists fileName then
|
||||
return! streamFile true fileName None None next ctx
|
||||
else
|
||||
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
|
||||
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 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 ->
|
||||
@ -83,7 +90,7 @@ let list : HttpHandler = fun next ctx -> task {
|
||||
| _ -> None
|
||||
{ DisplayUpload.id = ""
|
||||
name = name
|
||||
path = file.Substring(8).Replace (name, "")
|
||||
path = file.Replace($"{path}/", "").Replace (name, "")
|
||||
updatedOn = create
|
||||
source = UploadDestination.toString Disk
|
||||
})
|
||||
@ -93,7 +100,6 @@ let list : HttpHandler = fun next ctx -> task {
|
||||
| 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)
|
||||
@ -107,4 +113,4 @@ let list : HttpHandler = fun next ctx -> task {
|
||||
files = allFiles
|
||||
|}
|
||||
|> viewForTheme "admin" "upload-list" next ctx
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
<small>
|
||||
<a href="{{ post | relative_link }}" target="_blank">View Post</a>
|
||||
|
@ -1,24 +1,56 @@
|
||||
<h2 class="my-3">{{ page_title }}</h2>
|
||||
<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>
|
||||
<form method="post" class="container" hx-target="body">
|
||||
<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="col">File Name</div>
|
||||
<div class="col">Path</div>
|
||||
<div class="col">File Date/Time</div>
|
||||
<div class="col-6">File Name</div>
|
||||
<div class="col-3">Path</div>
|
||||
<div class="col-3">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 class="col-6">
|
||||
{%- capture badge_class -%}
|
||||
{%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
|
||||
{%- endcapture -%}
|
||||
{%- 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"> • 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 • </span>
|
||||
<a href="#" onclick="" class="text-danger">Delete</a>
|
||||
</small>
|
||||
</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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -220,6 +220,18 @@
|
||||
checkPodcast() {
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user