From d28a1bf31f92ad7b17d3552d828f851ebe8935d7 Mon Sep 17 00:00:00 2001 From: Martin Zurowietz Date: Thu, 14 Apr 2022 09:56:36 +0200 Subject: [PATCH] Add configurable maximum size of a single file --- .../Views/StorageRequestController.php | 2 ++ src/Http/Requests/StoreStorageRequestFile.php | 5 ++++- src/config/user_storage.php | 7 +++++++ src/public/assets/scripts/main.js | 2 +- src/public/mix-manifest.json | 2 +- src/resources/assets/js/createContainer.vue | 15 ++++++++++++++- src/resources/views/create.blade.php | 12 ++++++++++-- .../Api/StorageRequestFileControllerTest.php | 17 ++++++++++++++++- 8 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/Http/Controllers/Views/StorageRequestController.php b/src/Http/Controllers/Views/StorageRequestController.php index c6af761..339d4a6 100644 --- a/src/Http/Controllers/Views/StorageRequestController.php +++ b/src/Http/Controllers/Views/StorageRequestController.php @@ -54,6 +54,7 @@ public function create(Request $request) $user = User::convert($request->user()); $usedQuota = $user->storage_quota_used; $availableQuota = $user->storage_quota_available; + $maxFilesize = config('user_storage.max_file_size'); $previousRequest = StorageRequest::whereNull('submitted_at') ->where('user_id', $user->id) @@ -64,6 +65,7 @@ public function create(Request $request) 'previousRequest' => $previousRequest, 'usedQuota' => $usedQuota, 'availableQuota' => $availableQuota, + 'maxFilesize' => $maxFilesize, ]); } diff --git a/src/Http/Requests/StoreStorageRequestFile.php b/src/Http/Requests/StoreStorageRequestFile.php index c87535c..77ba918 100644 --- a/src/Http/Requests/StoreStorageRequestFile.php +++ b/src/Http/Requests/StoreStorageRequestFile.php @@ -39,8 +39,11 @@ public function rules() { $user = User::convert($this->storageRequest->user); + $maxQuota = $user->storage_quota_remaining; + $maxFile = config('user_storage.max_file_size'); + // The "max" rule expects kilobyte but the quota is in byte. - $maxKb = intval(round($user->storage_quota_remaining / 1024)); + $maxKb = intval(round(min($maxQuota, $maxFile) / 1000)); $mimes = implode(',', array_merge(Image::MIMES, Video::MIMES)); diff --git a/src/config/user_storage.php b/src/config/user_storage.php index 89fb608..ced4c7e 100644 --- a/src/config/user_storage.php +++ b/src/config/user_storage.php @@ -6,6 +6,13 @@ */ 'max_pending_requests' => env('USER_STORAGE_MAX_PENDING_REQUESTS', 3), + /* + | Maximum allowed size of a single uploaded file in bytes. + | + | Default: 5 GB + */ + 'max_file_size' => env('USER_STORAGE_MAX_FILE_SIZE', 5E+9), + /* | Allowed maximum combined file size for storage per user (in bytes). | diff --git a/src/public/assets/scripts/main.js b/src/public/assets/scripts/main.js index d0b7e87..32b5144 100644 --- a/src/public/assets/scripts/main.js +++ b/src/public/assets/scripts/main.js @@ -1 +1 @@ -(()=>{"use strict";var e,t={591:()=>{const e=Vue.resource("api/v1/storage-requests{/id}/directories"),t=Vue.resource("api/v1/storage-requests{/id}/files"),i=Vue.resource("api/v1/storage-requests{/id}",{},{approve:{method:"POST",url:"api/v1/storage-requests{/id}/approve"},reject:{method:"POST",url:"api/v1/storage-requests{/id}/reject"},extend:{method:"POST",url:"api/v1/storage-requests{/id}/extend"}});var s=biigle.$require("core.components.fileBrowser"),n=biigle.$require("messages").handleErrorResponse,r=biigle.$require("core.mixins.loader"),o=function(e){var t="",i=["kB","MB","GB","TB"];do{e/=1e3,t=i.shift()}while(e>1e3&&i.length>0);return"".concat(e.toFixed(2)," ").concat(t)},a=function(e){var t={name:"",directories:{},files:[]};return e.files&&e.files.forEach((function(e){var i=e.split("/"),s=i.pop(),n=t;i.forEach((function(e){n.directories.hasOwnProperty(e)||(n.directories[e]={name:e,directories:{},files:[]}),n=n.directories[e]})),n.files.push({name:s})})),t};function l(e,t,i,s,n,r,o,a){var l,u="function"==typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=i,u._compiled=!0),s&&(u.functional=!0),r&&(u._scopeId="data-v-"+r),o?(l=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(o)},u._ssrRegister=l):n&&(l=a?function(){n.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:n),l)if(u.functional){u._injectStyles=l;var d=u.render;u.render=function(e,t){return l.call(t),d(e,t)}}else{var c=u.beforeCreate;u.beforeCreate=c?[].concat(c,l):[l]}return{exports:e,options:u}}const u=l({mixins:[r],components:{fileBrowser:s},data:function(){return{currentUploadedSize:0,files:[],finished:!1,finishedUploadedSize:0,loadedUnfinishedRequest:!1,maxSize:-1,rootDirectory:{name:"",directories:{},files:[],selected:!1},selectedDirectory:null,storageRequest:null,usedQuotaBytes:0,availableQuotaBytes:0}},computed:{hasSelectedDirectory:function(){return null!==this.selectedDirectory},selectedDirectoryName:function(){return this.selectedDirectory.name},hasFiles:function(){return this.files.length>0},totalSize:function(){return this.files.reduce((function(e,t){return e+t.size}),0)},totalSizeForHumans:function(){return o(this.totalSize)},uploadedSize:function(){return this.currentUploadedSize+this.finishedUploadedSize},uploadedPercent:function(){return Math.round(this.uploadedSize/this.totalSize*100)},uploadedSizeForHumans:function(){return o(this.uploadedSize)},editable:function(){return!this.loading&&!this.finished},exceedsMaxSize:function(){return-1!==this.availableQuotaBytes&&this.totalSize>this.availableQuotaBytes},canSubmit:function(){return this.hasFiles&&!this.exceedsMaxSize},usedQuota:function(){return o(this.usedQuotaBytes)},availableQuota:function(){return o(this.availableQuotaBytes)},usedQuotaPercent:function(){return Math.round(this.usedQuotaBytes/this.availableQuotaBytes*100)}},methods:{handleFilesChosen:function(e){if(this.hasSelectedDirectory){var t=e.target.files,i=this.selectedDirectory.files,s=0,n=[];for(s=0;s0&&(this.addExistingFiles(this.storageRequest.files),this.loadedUnfinishedRequest=!0),window.addEventListener("beforeunload",(function(t){if(e.loading)return t.preventDefault(),t.returnValue="","This page is asking you to confirm that you want to leave - the file upload is still in progress."}))}},undefined,undefined,!1,null,null,null).exports;var d=l({props:{request:{type:Object,required:!0},expireDate:{type:Date,default:null},selected:{type:Boolean,default:!1}},computed:{pending:function(){return!this.request.expires_at},expired:function(){return Date.parse(this.request.expires_at){}},i={};function s(e){var n=i[e];if(void 0!==n)return n.exports;var r=i[e]={exports:{}};return t[e](r,r.exports,s),r.exports}s.m=t,e=[],s.O=(t,i,n,r)=>{if(!i){var o=1/0;for(d=0;d=r)&&Object.keys(s.O).every((e=>s.O[e](i[l])))?i.splice(l--,1):(a=!1,r0&&e[d-1][2]>r;d--)e[d]=e[d-1];e[d]=[i,n,r]},s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={355:0,392:0};s.O.j=t=>0===e[t];var t=(t,i)=>{var n,r,[o,a,l]=i,u=0;if(o.some((t=>0!==e[t]))){for(n in a)s.o(a,n)&&(s.m[n]=a[n]);if(l)var d=l(s)}for(t&&t(i);us(591)));var n=s.O(void 0,[392],(()=>s(401)));n=s.O(n)})(); \ No newline at end of file +(()=>{"use strict";var e,t={591:()=>{const e=Vue.resource("api/v1/storage-requests{/id}/directories"),t=Vue.resource("api/v1/storage-requests{/id}/files"),i=Vue.resource("api/v1/storage-requests{/id}",{},{approve:{method:"POST",url:"api/v1/storage-requests{/id}/approve"},reject:{method:"POST",url:"api/v1/storage-requests{/id}/reject"},extend:{method:"POST",url:"api/v1/storage-requests{/id}/extend"}});var s=biigle.$require("core.components.fileBrowser"),r=biigle.$require("messages").handleErrorResponse,n=biigle.$require("core.mixins.loader"),o=function(e){var t="",i=["kB","MB","GB","TB"];do{e/=1e3,t=i.shift()}while(e>1e3&&i.length>0);return"".concat(e.toFixed(2)," ").concat(t)},a=function(e){var t={name:"",directories:{},files:[]};return e.files&&e.files.forEach((function(e){var i=e.split("/"),s=i.pop(),r=t;i.forEach((function(e){r.directories.hasOwnProperty(e)||(r.directories[e]={name:e,directories:{},files:[]}),r=r.directories[e]})),r.files.push({name:s})})),t};function l(e,t,i,s,r,n,o,a){var l,u="function"==typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=i,u._compiled=!0),s&&(u.functional=!0),n&&(u._scopeId="data-v-"+n),o?(l=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),r&&r.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(o)},u._ssrRegister=l):r&&(l=a?function(){r.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:r),l)if(u.functional){u._injectStyles=l;var d=u.render;u.render=function(e,t){return l.call(t),d(e,t)}}else{var c=u.beforeCreate;u.beforeCreate=c?[].concat(c,l):[l]}return{exports:e,options:u}}const u=l({mixins:[n],components:{fileBrowser:s},data:function(){return{currentUploadedSize:0,files:[],finished:!1,finishedUploadedSize:0,loadedUnfinishedRequest:!1,maxSize:-1,rootDirectory:{name:"",directories:{},files:[],selected:!1},selectedDirectory:null,storageRequest:null,usedQuotaBytes:0,availableQuotaBytes:0,maxFilesizeBytes:0,exceedsMaxFilesize:!1}},computed:{hasSelectedDirectory:function(){return null!==this.selectedDirectory},selectedDirectoryName:function(){return this.selectedDirectory.name},hasFiles:function(){return this.files.length>0},totalSize:function(){return this.files.reduce((function(e,t){return e+t.size}),0)},totalSizeForHumans:function(){return o(this.totalSize)},uploadedSize:function(){return this.currentUploadedSize+this.finishedUploadedSize},uploadedPercent:function(){return Math.round(this.uploadedSize/this.totalSize*100)},uploadedSizeForHumans:function(){return o(this.uploadedSize)},editable:function(){return!this.loading&&!this.finished},exceedsMaxSize:function(){return-1!==this.availableQuotaBytes&&this.totalSize>this.availableQuotaBytes},canSubmit:function(){return this.hasFiles&&!this.exceedsMaxSize},usedQuota:function(){return o(this.usedQuotaBytes)},availableQuota:function(){return o(this.availableQuotaBytes)},usedQuotaPercent:function(){return Math.round(this.usedQuotaBytes/this.availableQuotaBytes*100)},maxFilesize:function(){return o(this.maxFilesizeBytes)}},methods:{handleFilesChosen:function(e){var t=this;if(this.hasSelectedDirectory){var i=Array.from(e.target.files).filter((function(e){return e.size<=t.maxFilesizeBytes}));i.length0&&(this.addExistingFiles(this.storageRequest.files),this.loadedUnfinishedRequest=!0),window.addEventListener("beforeunload",(function(t){if(e.loading)return t.preventDefault(),t.returnValue="","This page is asking you to confirm that you want to leave - the file upload is still in progress."}))}},undefined,undefined,!1,null,null,null).exports;var d=l({props:{request:{type:Object,required:!0},expireDate:{type:Date,default:null},selected:{type:Boolean,default:!1}},computed:{pending:function(){return!this.request.expires_at},expired:function(){return Date.parse(this.request.expires_at){}},i={};function s(e){var r=i[e];if(void 0!==r)return r.exports;var n=i[e]={exports:{}};return t[e](n,n.exports,s),n.exports}s.m=t,e=[],s.O=(t,i,r,n)=>{if(!i){var o=1/0;for(d=0;d=n)&&Object.keys(s.O).every((e=>s.O[e](i[l])))?i.splice(l--,1):(a=!1,n0&&e[d-1][2]>n;d--)e[d]=e[d-1];e[d]=[i,r,n]},s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={355:0,392:0};s.O.j=t=>0===e[t];var t=(t,i)=>{var r,n,[o,a,l]=i,u=0;if(o.some((t=>0!==e[t]))){for(r in a)s.o(a,r)&&(s.m[r]=a[r]);if(l)var d=l(s)}for(t&&t(i);us(591)));var r=s.O(void 0,[392],(()=>s(401)));r=s.O(r)})(); \ No newline at end of file diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json index bbce26f..922046c 100644 --- a/src/public/mix-manifest.json +++ b/src/public/mix-manifest.json @@ -1,4 +1,4 @@ { - "/assets/scripts/main.js": "/assets/scripts/main.js?id=f81abff4cf8ef6ae04d46776d5f83b25", + "/assets/scripts/main.js": "/assets/scripts/main.js?id=95755e02993c18b779164b925a9db6e5", "/assets/styles/main.css": "/assets/styles/main.css?id=16bca02d88eeae2a4e45dba3d77b7757" } diff --git a/src/resources/assets/js/createContainer.vue b/src/resources/assets/js/createContainer.vue index 99d2c14..044f02c 100644 --- a/src/resources/assets/js/createContainer.vue +++ b/src/resources/assets/js/createContainer.vue @@ -28,6 +28,8 @@ export default { storageRequest: null, usedQuotaBytes: 0, availableQuotaBytes: 0, + maxFilesizeBytes: 0, + exceedsMaxFilesize: false, }; }, computed: { @@ -75,6 +77,9 @@ export default { usedQuotaPercent() { return Math.round(this.usedQuotaBytes / this.availableQuotaBytes * 100); }, + maxFilesize() { + return sizeForHumans(this.maxFilesizeBytes); + }, }, methods: { handleFilesChosen(event) { @@ -85,7 +90,14 @@ export default { return; } - let newFiles = event.target.files; + let newFiles = Array.from(event.target.files).filter((file) => { + return file.size <= this.maxFilesizeBytes; + }); + + if (newFiles.length < event.target.files.length) { + this.exceedsMaxFilesize = true; + } + let files = this.selectedDirectory.files; let i = 0; @@ -333,6 +345,7 @@ export default { created() { this.usedQuotaBytes = biigle.$require('user-storage.usedQuota'); this.availableQuotaBytes = biigle.$require('user-storage.availableQuota'); + this.maxFilesizeBytes = biigle.$require('user-storage.maxFilesize'); // This remains null if no previous request exists. this.storageRequest = biigle.$require('user-storage.previousRequest'); if (this.storageRequest && this.storageRequest.files.length > 0) { diff --git a/src/resources/views/create.blade.php b/src/resources/views/create.blade.php index b07e6b2..4e951c1 100644 --- a/src/resources/views/create.blade.php +++ b/src/resources/views/create.blade.php @@ -8,6 +8,7 @@ biigle.$declare('user-storage.previousRequest', {!! $previousRequest ?? 'null' !!}); biigle.$declare('user-storage.usedQuota', {!! $usedQuota !!}); biigle.$declare('user-storage.availableQuota', {!! $availableQuota !!}); + biigle.$declare('user-storage.maxFilesize', {!! $maxFilesize !!}); @endpush @@ -57,7 +58,7 @@ class="hidden" v-on:input="handleFilesChosen" > -
+
Uploaded of @@ -118,7 +119,6 @@ class="btn btn-success" v-bind:disabled="exceedsMaxSize" > Submit -
diff --git a/tests/Http/Controllers/Api/StorageRequestFileControllerTest.php b/tests/Http/Controllers/Api/StorageRequestFileControllerTest.php index aad7cf0..058732c 100644 --- a/tests/Http/Controllers/Api/StorageRequestFileControllerTest.php +++ b/tests/Http/Controllers/Api/StorageRequestFileControllerTest.php @@ -87,7 +87,7 @@ public function testStorePrefix() $this->assertSame(['abc/def/test.jpg'], $request->fresh()->files); } - public function testStoreTooLarge() + public function testStoreTooLargeQuota() { config(['user_storage.pending_disk' => 'test']); config(['user_storage.user_quota' => 10000]); @@ -102,6 +102,21 @@ public function testStoreTooLarge() ->assertStatus(422); } + public function testStoreTooLargeFile() + { + config(['user_storage.pending_disk' => 'test']); + config(['user_storage.max_file_size' => 10000]); + $disk = Storage::fake('test'); + + $request = StorageRequest::factory()->create(); + $id = $request->id; + + $file = new UploadedFile(__DIR__."/../../../files/test.jpg", 'test.jpg', 'image/jpeg', null, true); + $this->be($request->user); + $this->postJson("/api/v1/storage-requests/{$id}/files", ['file' => $file]) + ->assertStatus(422); + } + public function testStoreMimeType() { config(['user_storage.pending_disk' => 'test']);