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 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"> • 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>
|
||||||
<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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user