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) | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -221,6 +221,18 @@ | |||||||
|     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 | ||||||
|    * @param source The source that was selected |    * @param source The source that was selected | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user