File Upload / Temp File
When a form contains a file upload, you might want to store this file temporarly. Confinity offers a service for temporary files to simplify this process.
Warning
Use caution when providing users with the ability to upload files to a server. Attackers may attempt to:
- Execute denial of service attacks.
- Upload viruses or malware.
- Compromise networks and servers in other ways (precautions made by Confinity).
Confinity offers a simplified and relatively secure upload. But still DDOS attacks are not handled.
Configuration in code
Register your space and configure it in code:
// Provide your antivirus scanner for security scanning (required).
module.Services.AddTransient<IAntivirusScanner, MyAwesomeAntivirus>();
module.Configure.RegisterTempFileSpace("upload-doc", options =>
{
options
.SpaceCapacity(25 * 1024 * 1024)
.SessionCapacity(2 * 1024 * 1024)
.FileCapacity(1 * 1024 * 1024)
.MaxAge(TimeSpan.FromSeconds(30))
.AllowFileTypeZip()
.AllowFileTypeJpg()
.AllowFileTypePng()
.AllowFileType(".mp3",
[
[0xFF, 0xFB],
[0xFF, 0xF3],
[0xFF, 0xF2],
[0x49, 0x44, 0x33],
]);
});
Configuration in code & app-settings
Register your space and configure in app-settings:
// Provide your antivirus scanner for security scanning (required).
module.Services.AddTransient<IAntivirusScanner, MyAwesomeAntivirus>();
module.Configure.RegisterTempFileSpace("upload-doc");
app-settings:
{
"ConfinityTempFile": {
"upload-doc": {
"SpaceCapacityInBytes": 209715200,
"SessionCapacityInBytes": 20971520,
"FileCapacityInBytes": 4194304,
"MaxAgeInSeconds": 20,
"PermittedFileSignatures": {"pdf":[[37,80,68,70,45]]}
}
}
}
| Configuration Key | Description |
|---|---|
| SpaceCapacityInBytes | How much storage this space can take. This is measured over all current uploads, which have not reached the max age. |
| SessionCapacityInBytes | How many bytes can be stored in a session. |
| FileCapacityInBytes | How big a single file can be. |
| MaxAgeInSeconds | How long a file is allowed to live without a keep alive. This shouldn't be to big so that old sessions can be cleaned up. |
| AllowedFileTypes | An object of allowed file types. key: Extension with leading dot. value: a list of allowed magic byte sequences. Example: ".jpg": "[[255,216,255,224],[255,216,255,225],[255,216,255,226],[255,216,255,227],[255,216,255,254]]" |
Usage
For the temp file to work you need a controller and a kind of a view. This way you can customize the needed authorization.
Controller
The controller needs to implement these three methods:
- File (route 'file') which allows file uploads
- Keep (route 'keep') which prevent automatic deletion when the user is idle
- Remove (route 'remove') which removes an uploaded file
To assist you should implement ITempFileController.
Your implementation should look like this:
private readonly ITempFileService _tempFileService;
public TempFileDocController(ITempFileService tempFileService)
{
_tempFileService = tempFileService;
}
[HttpPost("file")]
[RequestSizeLimit(ITempFileService.RequestSizeLimit)]
[RequestFormLimits(MultipartBodyLengthLimit = ITempFileService.MaxChunkSize)]
[ProducesResponseType(typeof(TempFileChunkUpload), (int)HttpStatusCode.OK)]
public async Task<IActionResult> File([FromForm(Name = "file")] IFormFile file, [FromForm] int chunkId,
[FromForm] string sessionId, [FromForm] Guid? fileId, [FromForm] string? contextPayload = null)
{
var result =
await _tempFileService.StoreAsync(sessionId, "upload-doc", file, chunkId, fileId);
return Ok(result);
}
[HttpPost("keep")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> Keep([FromForm] string sessionId)
{
await _tempFileService.KeepAliveAsync(sessionId, "upload-doc");
return Ok();
}
[HttpPost("remove")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> Remove([FromForm] string sessionId, [FromForm] Guid fileId)
{
await _tempFileService.RemoveAsync(sessionId, "upload-doc", fileId);
return Ok();
}
View
In the view you have to request a Session Id. This is done by calling ITempFileService.GenerateSessionId().
For the upload you can use our web component or just use the javascript client.
Web Component (easy but not everything is customizable)
- use the component in your html:
*@
<confinity-file-upload label="Dateien auswählen" api="/api/v1/upload/demo" sessionId="@_tempFileService.GenerateSessionId()"></confinity-file-upload>
@*
- use our javascript:
*@
/** Embed these two web components
* customElements.define('confinity-file-upload-item', ConfinityFileUploadItem);
* customElements.define('confinity-file-upload', ConfinityFileUpload);
*/
var u=Object.defineProperty;var v=(l,t,e)=>t in l?u(l,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):l[t]=e;var i=(l,t,e)=>(v(l,typeof t!="symbol"?t+"":t,e),e);import{F as p,T as f}from"./TempFileClient.js";function w(l){const t=l<0,e=["B","KB","MB","GB","TB","PB","EB","ZB","YB"];if(t&&(l=-l),l<1)return(t?"-":"")+l+" B";const s=Math.min(Math.floor(Math.log(l)/Math.log(1e3)),e.length-1);l=l/Math.pow(1e3,s);const r=e[s];return(t?"-":"")+l.toFixed(1)+" "+r}const m=class extends HTMLElement{constructor(t,e,s){super();i(this,"_name");i(this,"filenameAttr");i(this,"sizeAttr");i(this,"_size");i(this,"errorEl");i(this,"stateEl");i(this,"progressEl");i(this,"tempFileId");i(this,"_file");i(this,"iconCheck",'<svg viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"><circle cx="16" cy="16" r="15"/><polyline points="7 19 11 23 25 9"/></g></svg>');i(this,"iconError",'<svg fill="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6zM7.9 7.5L10.3 5l.7.7-2.4 2.5 2.4 2.5-.7.7-2.4-2.5-2.4 2.5-.7-.7 2.4-2.5-2.4-2.5.7-.7 2.4 2.5z" clip-rule="evenodd" fill-rule="evenodd"/></svg>');i(this,"iconNew",'<svg viewBox="0 0 485 485" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="85.285" y="192.5" width="200" height="30"/><path d="m350.28 227.02v-118.23l-108.79-108.79h-221.21v445h221.52c23.578 24.635 56.766 40 93.478 40 71.368 0 129.43-58.062 129.43-129.43 0-66.294-50.103-121.1-114.43-128.56zm-100-175.8 48.787 48.787h-48.787v-48.787zm-200 363.79v-385h170v100h100v97.015c-28.908 3.352-54.941 16.262-74.846 35.485h-160.15v30h137.01c-6.865 12.25-11.802 25.719-14.382 40h-122.63v30h120.76c0.999 18.842 6.049 36.626 14.289 52.5h-170.05zm285 40c-54.826 0-99.43-44.604-99.43-99.43s44.604-99.429 99.43-99.429 99.43 44.604 99.43 99.429-44.604 99.43-99.43 99.43z"/><polygon points="350.28 293.96 320.28 293.96 320.28 340.57 273.67 340.57 273.67 370.57 320.28 370.57 320.28 417.19 350.28 417.19 350.28 370.57 396.9 370.57 396.9 340.57 350.28 340.57"/></svg>');i(this,"iconUploading",'<svg viewBox="0 0 485 485" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="85.285" y="192.5" width="200" height="30"/><path d="m350.28 227.02v-118.23l-108.79-108.79h-221.21v445h221.52c23.578 24.635 56.766 40 93.478 40 71.368 0 129.43-58.062 129.43-129.43 0-66.294-50.103-121.1-114.43-128.56zm-100-175.8 48.787 48.787h-48.787v-48.787zm-200 363.79v-385h170v100h100v97.015c-28.908 3.352-54.941 16.262-74.846 35.485h-160.15v30h137.01c-6.865 12.25-11.802 25.719-14.382 40h-122.63v30h120.76c0.999 18.842 6.049 36.626 14.289 52.5h-170.05zm285 40c-54.826 0-99.43-44.604-99.43-99.43s44.604-99.429 99.43-99.429 99.43 44.604 99.43 99.429-44.604 99.43-99.43 99.43z"/><path d="m334.82 278.39c-19.648 21.433-40.591 41.625-61.145 62.179v30l46.61-46.706v93.326h30v-94.302l46.62 47.682v-30c-20.724-20.698-41.444-41.399-62.085-62.179z"/></svg>');i(this,"_inputName");var g;this._inputName=e;const r=document.createElement("div");r.style.position="relative";const n=document.createElement("div");n.style.display="flex",n.style.justifyContent="space-between",n.style.whiteSpace="break-word",this.stateEl=document.createElement("div"),n.appendChild(this.stateEl),this.filenameAttr=document.createElement("div");const a=this.getAttribute(m.AttrFile);this.filenameAttr.textContent=(g=a!=null?a:t==null?void 0:t.name)!=null?g:"",this.filenameAttr.classList.add(m.AttrFile),this.filenameAttr.style.overflow="hidden",this.filenameAttr.style.wordBreak="break-all",this.filenameAttr.style.flexGrow="1",n.appendChild(this.filenameAttr);const o=document.createElement("span");this.sizeAttr=document.createElement("span"),this.sizeAttr.classList.add(m.AttrSize),this._file=t,this.size=t.size,o.style.whiteSpace="nowrap",o.style.paddingLeft=".5rem",o.style.textAlign="right",o.appendChild(this.sizeAttr);const h=document.createElement("span");h.style.paddingLeft=".25rem",h.innerText="X",h.style.color="var(--f-color-error)",h.style.cursor="pointer";const d=s!=null?s:h;d.addEventListener("click",()=>{const E=new CustomEvent("remove-file",{detail:this,bubbles:!0,cancelable:!1,composed:!0});this.dispatchEvent(E)},!1),o.appendChild(d),n.appendChild(o),r.appendChild(n),this.progressEl=document.createElement("div"),this.progressEl.style.position="absolute",this.progressEl.style.bottom=".125rem",this.progressEl.style.left="0",this.progressEl.style.width="0",this.progressEl.style.height=".125rem",this.progressEl.style.marginBottom=".125rem",this.progressEl.style.transition="width ease 1s, opacity ease 2s",this.progressEl.style.backgroundColor="var(--f-color-highlight)",this.progressEl.style.color="var(--f-color-on-highlight);",r.appendChild(this.progressEl),this.errorEl=document.createElement("div"),r.appendChild(this.errorEl),this.attachShadow({mode:"open"}).appendChild(r),this.state=p.NEW}set state(t){this.stateEl.style.paddingRight=".5rem",this.stateEl.style.width="1.5rem",t==p.UPLOADED?this.stateEl.innerHTML=this.iconCheck:t==p.NEW?this.stateEl.innerHTML=this.iconNew:t==p.UPLOADING?this.stateEl.innerHTML=this.iconUploading:t==p.ERROR?this.stateEl.innerHTML=this.iconError:this.stateEl.innerText=t+""}get file(){return this._file}set name(t){this._name=t,this.filenameAttr.innerText=t}set error(t){this.errorEl.innerText=t,this.progressEl.style.opacity="0"}set size(t){this._size=t,this.sizeAttr.innerText=w(t)}set progress(t){this.progressEl.style.opacity="1",this.progressEl.style.width=t*100+"%",t===1&&(this.progressEl.style.opacity="0")}attributeChangedCallback(t,e,s){console.log("attrChange",t,e,s)}onState(t){this.state=t}onProgress(t){this.progress=t}onUploadError(t,e,s){this.error=e}onUploadFinished(t){var s;const e=document.createElement("input");e.type="hidden",e.name=this._inputName,e.value=t.tempFileId+"",(s=this.shadowRoot)==null||s.appendChild(e)}};let c=m;i(c,"AttrSize","size"),i(c,"AttrFile","file");class y extends HTMLElement{constructor(){super();i(this,"files");i(this,"UserDraggingOverClass","userDraggingOver");i(this,"api","");i(this,"sessionId","sessionId");i(this,"maxAgeS",10);i(this,"_inputName","");i(this,"rootEl");i(this,"inputEl");i(this,"labelP");this.files=new f,this.rootEl=document.createElement("label");const t=this.rootEl;t.style.cursor="pointer";const e=document.createElement("input");this.inputEl=e;const s=document.createElement("slot");t.appendChild(e),this.labelP=document.createElement("p"),s.appendChild(this.labelP),this.labelP.innerText="Choose files",t.appendChild(s);const r=this.attachShadow({mode:"open"});r.appendChild(t),r.querySelector("input").addEventListener("change",n=>{const a=n.target;for(let o of a.files)this.add(o);a.value=""}),this.addEventListener("dragover",this.onDragOver),this.addEventListener("dragleave",this.onDragLeave,!1),this.addEventListener("drop",this.onDrop),this.addEventListener("remove-file",this.onRemoveFile)}connectedCallback(){var r,n,a,o,h,d;console.log("Custom square element added to page.");const t=(r=this.getAttribute("label"))!=null?r:"Choose files",e=this.getAttribute("multiple")!==void 0;this.api=(n=this.getAttribute("api"))!=null?n:"",this.sessionId=(a=this.getAttribute("sessionId"))!=null?a:"",this.maxAgeS=parseInt((o=this.getAttribute("maxAgeS"))!=null?o:"60");const s=parseInt((h=this.getAttribute("concurrent"))!=null?h:"2");this._inputName=(d=this.getAttribute("input-name"))!=null?d:"confinity-temp-file",e&&this.inputEl.setAttribute("multiple","multiple"),this.inputEl.setAttribute("type","file"),this.inputEl.style.display="none",this.inputEl.name=this._inputName,this.files=new f({api:this.api,concurrent:s,maxAgeS:this.maxAgeS,sessionId:this.sessionId}),this.labelP.innerText=t}onRemoveFile(t){var s;const e=t.detail;this.files.remove(e.file),(s=this.shadowRoot)==null||s.removeChild(e)}add(t){var e;if(!this.files.contains(t)){const s=this.files.add(t),r=new c(s,this._inputName);s.listener=r,this.append(r),(e=this.shadowRoot)==null||e.appendChild(r)}}onDragLeave(){this.classList.remove(this.UserDraggingOverClass)}onDragOver(t){t.preventDefault(),this.classList.add(this.UserDraggingOverClass)}onDrop(t){if(t.preventDefault(),this.classList.remove(this.UserDraggingOverClass),t.dataTransfer!=null)if(t.dataTransfer.items){for(let e=0;e<t.dataTransfer.items.length;e++)if(t.dataTransfer.items[e].kind==="file"){const s=t.dataTransfer.items[e].getAsFile();if(s==null)return;this.add(s)}}else for(let e=0;e<t.dataTransfer.files.length;e++)this.add(t.dataTransfer.files[e])}}customElements.define("confinity-file-upload-item",c);customElements.define("confinity-file-upload",y);
@*
The web component will add the uploaded files as hidden input to the DOM so that the form-submit adds them. The value is the TempFile.Id.
Required attributes:
- api: path to your controller
- sessionId: session id (
ITempFileService.GenerateSessionId())
Optional attributes
- maxAgeS: how long the files are allowed to stay without automatic delete (Default: 60)
- concurrent: how many files are uploaded concurrent (Default: 2)
- input-name: what the name is of the hidden input (Default: 'confinity-temp-file')
Stlying
- The class 'userDraggingOver' is added when the user drags files over the input.
Javascript Client (hard but everything is customizable)
Example usage:
*@
/** Import the upload client to provider your own upload component.
* The client is accessible from ConfinityTempFileClient
*/
var U=Object.defineProperty;var g=(n,e,t)=>e in n?U(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var i=(n,e,t)=>(g(n,typeof e!="symbol"?e+"":e,t),t);var r;(function(n){n[n.NEW=10]="NEW",n[n.UPLOADING=20]="UPLOADING",n[n.UPLOADED=30]="UPLOADED",n[n.ERROR=90]="ERROR"})(r||(r={}));const d=class{constructor(e){i(this,"chunks");i(this,"_file");this.chunks=[],this._file=e;const t=Math.ceil(e.size/d.CHUNK_SIZE);for(let s=0;s< t;s++)this.chunks.push(e.slice(s*d.CHUNK_SIZE,Math.min(s*d.CHUNK_SIZE+d.CHUNK_SIZE,e.size),e.type))}get originalFile(){return this._file}};let p=d;i(p,"CHUNK_SIZE",2*1024*1024);class m extends Error{constructor(e,t,s){super(t);i(this,"fileContainer");i(this,"errorName");this.errorName=e,this.fileContainer=s}}class y{constructor(e){i(this,"_state",r.NEW);i(this,"queue");i(this,"_tempFileId");i(this,"listener",null);this.state=r.NEW,this.queue=new p(e)}get tempFileId(){return this._tempFileId}get file(){return this.queue.originalFile}get size(){return this.queue.originalFile.size}get name(){return this.queue.originalFile.name}set state(e){var t;this._state=e,(t=this.listener)==null||t.onState(this._state)}isSame(e){return this.queue.originalFile.name===e.name}async upload(e,t){var o,l;this.state=r.UPLOADING;let s=0;for(const h of this.queue.chunks){const u=new FormData;u.append("file",h,this.name),u.append("chunkId",s+++""),u.append("sessionId",t),this._tempFileId&&u.append("fileId",this._tempFileId),await fetch(e,{method:"POST",body:u}).then(async c=>{var I;if(c.ok){const a=await c.json();this._tempFileId=a.tempFileId}else{this.state=r.ERROR;const a=await c.json(),f=new m(a.errorName,a.message,this);throw(I=this.listener)==null||I.onUploadError(this,a.errorName,a.message),f}return c});const _=this.queue.chunks.length;(o=this.listener)==null||o.onProgress(s/_),this.state=s===_?r.UPLOADED:r.UPLOADING}return(l=this.listener)==null||l.onUploadFinished(this),this}async remove(e,t){if(!this._tempFileId)return;const s=new FormData;s.append("sessionId",t),s.append("fileId",this._tempFileId),await fetch(e,{method:"POST",body:s})}}class F{constructor(e){i(this,"_api");i(this,"_sessionId");i(this,"_maxAgeS");i(this,"files",[]);i(this,"_totalBytes",0);i(this,"_toUpload",[]);i(this,"_concurrent");i(this,"_currentUpload",0);i(this,"uploadingFiles",!1);i(this,"keepInterval",0);i(this,"listener");var t,s,o,l;this._api=(t=e==null?void 0:e.api)!=null?t:"",this._sessionId=(s=e==null?void 0:e.sessionId)!=null?s:"",this._maxAgeS=(o=e==null?void 0:e.maxAgeS)!=null?o:60,this._concurrent=(l=e==null?void 0:e.concurrent)!=null?l:2}add(e){var s;const t=new y(e);return this.files.push(t),this.totalBytes+=t.size,this.addToUploadQueue(t),(s=this.listener)==null||s.onAdd(t),t}set totalBytes(e){var t;this._totalBytes=e,(t=this.listener)==null||t.onTotalBytes(this._totalBytes)}get totalBytes(){return this._totalBytes}contains(e){return this.files.findIndex(t=>t.isSame(e))!==-1}remove(e){var o,l;const t=this.files.findIndex(h=>h.isSame(e.file));if(t!==-1){const h=this.files.splice(t,1)[0];h.remove(this._api+"/remove",this._sessionId),this.totalBytes-=h.size,(o=this.listener)==null||o.onRemove(e)}const s=this._toUpload.findIndex(h=>h.isSame(e.file));s!==-1&&this._toUpload.splice(s,1),(l=this.listener)==null||l.onTotalBytes(this._totalBytes)}addToUploadQueue(e){this._toUpload.push(e),this.triggerUpload()}async triggerUpload(){if(this.keepFiles(),this.uploadingFiles=!0,this._currentUpload< this._concurrent && this._toUpload.length>0){this._currentUpload++;const e=this._toUpload.pop();if(e){try{await this.upload(e)}catch(t){console.warn(t)}this._currentUpload--}await this.triggerUpload()}}async upload(e){return await e.upload(this._api+"/file",this._sessionId)}keepFiles(){!this.keepInterval && this.uploadingFiles&&(this.keepInterval=window.setInterval(async()=>{const e=new FormData;e.append("sessionId",this._sessionId),await fetch(this._api+"/keep",{method:"POST",body:e})},this._maxAgeS/3*2*1e3))}}export{r as F,F as T};window.ConfinityTempFileClient=T;
new ConfinityTempFileClient(...
@*
To use the Javascript client the following implemented interfaces should help:
- Create a client of ConfinityTempFileClient
const client = new ConfinityTempFileClient({params})
import {IFilesContainerListener} from './IFilesContainerListener';
import {FileContainer} from './FileContainer';
export interface IConfinityTempFileClient {
/**
* Implementation of your listener to react on events.
*/
listener?: IFilesContainerListener;
/**
* Add a file for uploading and starts the upload.
* @param file the created FileContainer. Set the 'listener' property to react on changes. Your listener should implement IFileContainer.
* @param contextPayload some optional context payload.
*/
add(file: File, contextPayload?: any): FileContainer;
/**
* Checks if a file is already added.
* @param file
*/
contains(file: File): boolean;
/**
* Removes an existing file and cancels uploads
* @param fileContainer
*/
remove(fileContainer: FileContainer): void;
/**
* Returns all files
*/
files(): FileContainer[];
}
- The client takes the following configuration object:
export interface IConfinityTempFileParams {
/**
* Path to the controller
*/
api: string;
/**
* The sessionId provided by the ITempFileService
*/
sessionId: string;
/**
* The max age of a file as configured in the module
*/
maxAgeS?: number;
/**
* How many concurrent uploads will take place at once.
*/
concurrent?: number;
}
- Set a listener on the client to react on events.
client.listener = { onTotalBytes: (total) => {console.log('total bytes: ' + total);}
import {FileContainer} from "./FileContainer";
export interface IFilesContainerListener {
/**
* Gets calles when the total bytes changes.
* @param totalBytes
*/
onTotalBytes(totalBytes: number): void
/**
* Gets called, when a file is added.
* @param fileContainer
*/
onAdd(fileContainer: FileContainer): void
/**
* Get called when a file is removed.
* @param fileContainer
*/
onRemove(fileContainer: FileContainer): void
}
- Set a listener on the added file to react on its events.
const file = client.add(myFile);
file.listener = {
onUploadFinished: (fileContainer) => {
console.log('upload finished!')
}
}
import {FileContainerState} from "./FileContainerState";
import {FileContainer} from "./FileContainer";
export interface IFileContainer {
/**
* Is called when the state changes.
* @param value
*/
onState(value: FileContainerState): void;
/**
* Called when progress is changed.
* @param value 0..1 of upload
*/
onProgress(value: number): void;
/**
* Called when the upload has finished
* @param fileContainer
*/
onUploadFinished(fileContainer: FileContainer): void
/**
* Called when the upload had an error.
* @param fileContainer
* @param errorName
* @param errorDescription
*/
onUploadError(fileContainer: FileContainer, errorName: string, errorDescription: string): void
}