diff --git a/src/Controller/DumpController.php b/src/Controller/DumpController.php index 702dae0..fe12eb9 100644 --- a/src/Controller/DumpController.php +++ b/src/Controller/DumpController.php @@ -9,8 +9,11 @@ use AeonDumpManager\Service\DumpService; use AeonDumpManager\Service\Exception\DumpNotFoundException; use Shopware\Core\Framework\Routing\ApiRouteScope; use Shopware\Core\PlatformRequest; +use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; @@ -77,6 +80,32 @@ class DumpController return new JsonResponse(null, Response::HTTP_NO_CONTENT); } + #[Route( + path: '/api/_action/aeon-dump-manager/dumps/{filename}/download', + name: 'api.action.aeon_dump_manager.dumps.download', + defaults: [PlatformRequest::ATTRIBUTE_ACL => ['aeon_dump_manager.viewer']], + methods: ['GET'] + )] + public function download(string $filename): StreamedResponse + { + try { + $stream = $this->dumpService->readStream($filename); + } catch (DumpNotFoundException $exception) { + throw new NotFoundHttpException($exception->getMessage()); + } + + $response = new StreamedResponse(static function () use ($stream): void { + fpassthru($stream); + }); + $response->headers->set('Content-Type', 'application/gzip'); + $response->headers->set( + 'Content-Disposition', + HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename) + ); + + return $response; + } + #[Route( path: '/api/_action/aeon-dump-manager/dumps/jobs/{jobId}', name: 'api.action.aeon_dump_manager.dumps.jobs.status', diff --git a/src/Resources/app/administration/src/module/aeon-dump-manager/page/aeon-dump-manager-list/aeon-dump-manager-list.html.twig b/src/Resources/app/administration/src/module/aeon-dump-manager/page/aeon-dump-manager-list/aeon-dump-manager-list.html.twig index 056dba2..2d00b1a 100644 --- a/src/Resources/app/administration/src/module/aeon-dump-manager/page/aeon-dump-manager-list/aeon-dump-manager-list.html.twig +++ b/src/Resources/app/administration/src/module/aeon-dump-manager/page/aeon-dump-manager-list/aeon-dump-manager-list.html.twig @@ -4,6 +4,7 @@ @@ -12,54 +13,99 @@ + {% block aeon_dump_manager_list_progress_modal %} + +
+ + + +
+
+ {% endblock %} + ; pollTimer: number | null; } @@ -25,11 +28,15 @@ interface ComponentInstance extends ComponentData { loadList: () => Promise; stopPolling: () => void; pollJobStatus: (jobId: string) => void; + selectionCount: number; } Component.register('aeon-dump-manager-list', { template, + // Provides createNotificationSuccess/createNotificationError, used below. + mixins: [Mixin.getByName('notification')], + inject: ['aeonDumpManagerApiService', 'acl'], data(): ComponentData { @@ -37,7 +44,8 @@ Component.register('aeon-dump-manager-list', { dumps: [], isLoading: false, pendingJob: null, - deleteFilename: null, + deleteFilenames: null, + selection: {}, pollTimer: null, }; }, @@ -63,6 +71,10 @@ Component.register('aeon-dump-manager-list', { }, ]; }, + + selectionCount(this: ComponentInstance): number { + return Object.keys(this.selection).length; + }, }, created(this: ComponentInstance) { @@ -94,6 +106,10 @@ Component.register('aeon-dump-manager-list', { return Shopware.Utils.format.fileSize(bytes); }, + onSelectionChange(this: ComponentInstance, selection: Record) { + this.selection = selection; + }, + onCreate(this: ComponentInstance) { this.pendingJob = { percent: 0, status: 'pending' }; @@ -102,10 +118,9 @@ Component.register('aeon-dump-manager-list', { this.pollJobStatus(response.jobId); }) .catch((error: Error) => { - this.pendingJob = null; - this.createNotificationError({ - message: this.$tc('aeon-dump-manager.list.createFailed', 0, { error: error.message }), - }); + // Shown in the progress modal itself, not a notification — + // stays open so the user can actually read what went wrong. + this.pendingJob = { status: 'failed', percent: null, errorMessage: error.message }; }); }, @@ -114,24 +129,20 @@ Component.register('aeon-dump-manager-list', { this.pollTimer = window.setInterval(() => { this.aeonDumpManagerApiService.getJobStatus(jobId).then((job: DumpJob) => { + this.pendingJob = job; + if (job.status === 'done') { this.stopPolling(); - this.pendingJob = null; - this.createNotificationSuccess({ message: this.$tc('aeon-dump-manager.list.createSuccess') }); this.loadList(); + window.setTimeout(() => { + this.pendingJob = null; + }, CREATE_MODAL_AUTO_CLOSE_MS); return; } if (job.status === 'failed') { this.stopPolling(); - this.pendingJob = null; - this.createNotificationError({ - message: this.$tc('aeon-dump-manager.list.createFailed', 0, { error: job.errorMessage ?? '' }), - }); - return; } - - this.pendingJob = job; }); }, 2000); }, @@ -143,20 +154,62 @@ Component.register('aeon-dump-manager-list', { } }, + onCloseProgressModal(this: ComponentInstance) { + this.stopPolling(); + this.pendingJob = null; + }, + + onDownload(this: ComponentInstance, filename: string) { + this.aeonDumpManagerApiService.download(filename) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }) + .catch((error: Error) => { + this.createNotificationError({ + message: this.$tc('aeon-dump-manager.list.downloadFailed', 0, { error: error.message }), + }); + }); + }, + onRequestDelete(this: ComponentInstance, filename: string) { - this.deleteFilename = filename; + this.deleteFilenames = [filename]; + }, + + onRequestBulkDelete(this: ComponentInstance) { + this.deleteFilenames = Object.keys(this.selection); }, onCancelDelete(this: ComponentInstance) { - this.deleteFilename = null; + this.deleteFilenames = null; }, onConfirmDelete(this: ComponentInstance): Promise { - const filename = this.deleteFilename as string; - this.deleteFilename = null; + const filenames = this.deleteFilenames as string[]; + this.deleteFilenames = null; - return this.aeonDumpManagerApiService.remove(filename) - .then(() => this.loadList()); + // Optimistic UI update: pull the rows out immediately rather than + // waiting on a full reload, so the grid never looks stale even if + // loadList() is slow or a later delete in the batch fails. + this.dumps = this.dumps.filter((dumpFile) => !filenames.includes(dumpFile.filename)); + this.selection = {}; + + return Promise.all(filenames.map((filename) => this.aeonDumpManagerApiService.remove(filename))) + .then(() => this.loadList()) + .catch((error: Error) => { + this.createNotificationError({ + message: this.$tc('aeon-dump-manager.list.deleteFailed', 0, { error: error.message }), + }); + // A delete failed after the optimistic removal — reload to + // show the real state rather than leave a wrong guess on screen. + return this.loadList(); + }); }, }, }); diff --git a/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/de-DE.json b/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/de-DE.json index 1413412..827d874 100644 --- a/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/de-DE.json +++ b/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/de-DE.json @@ -9,13 +9,19 @@ "columnDatetime": "Erstellt am", "columnSize": "Größe", "buttonCreate": "Dump erstellen", + "buttonDownload": "Herunterladen", "buttonDelete": "Löschen", + "bulkDeleteButton": "Auswahl löschen ({count})", "confirmDeleteTitle": "Dump löschen?", "confirmDeleteText": "Möchten Sie \"{filename}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.", + "confirmDeleteTextMultiple": "Möchten Sie {count} Dumps wirklich löschen? Dies kann nicht rückgängig gemacht werden.", "empty": "Keine Dumps gefunden.", - "creating": "Dump wird erstellt…", + "progressTitle": "Dump wird erstellt", + "progressText": "Dump wird erstellt…", "createFailed": "Dump konnte nicht erstellt werden: {error}", - "createSuccess": "Dump erstellt." + "createSuccess": "Dump erstellt.", + "deleteFailed": "Dump konnte nicht gelöscht werden: {error}", + "downloadFailed": "Dump konnte nicht heruntergeladen werden: {error}" } } } diff --git a/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/en-GB.json b/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/en-GB.json index b75e6d1..c2a375d 100644 --- a/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/en-GB.json +++ b/src/Resources/app/administration/src/module/aeon-dump-manager/snippet/en-GB.json @@ -9,13 +9,19 @@ "columnDatetime": "Created at", "columnSize": "Size", "buttonCreate": "Create dump", + "buttonDownload": "Download", "buttonDelete": "Delete", + "bulkDeleteButton": "Delete selected ({count})", "confirmDeleteTitle": "Delete dump?", "confirmDeleteText": "Are you sure you want to delete \"{filename}\"? This cannot be undone.", + "confirmDeleteTextMultiple": "Are you sure you want to delete {count} dumps? This cannot be undone.", "empty": "No dumps found.", - "creating": "Creating dump…", + "progressTitle": "Creating dump", + "progressText": "Creating dump…", "createFailed": "Could not create dump: {error}", - "createSuccess": "Dump created." + "createSuccess": "Dump created.", + "deleteFailed": "Could not delete dump: {error}", + "downloadFailed": "Could not download dump: {error}" } } } diff --git a/src/Resources/app/administration/src/service/dump-api.service.ts b/src/Resources/app/administration/src/service/dump-api.service.ts index 1f0ef0e..32697df 100644 --- a/src/Resources/app/administration/src/service/dump-api.service.ts +++ b/src/Resources/app/administration/src/service/dump-api.service.ts @@ -66,4 +66,13 @@ export default class DumpApiService extends ApiService { .get(`/_action/aeon-dump-manager/dumps/jobs/${encodeURIComponent(jobId)}`, { headers: this.getBasicHeaders() }) .then((response: unknown) => ApiService.handleResponse(response)); } + + download(filename: string): Promise { + return this.httpClient + .get(`/_action/aeon-dump-manager/dumps/${encodeURIComponent(filename)}/download`, { + headers: this.getBasicHeaders(), + responseType: 'blob', + }) + .then((response: unknown) => ApiService.handleResponse(response) as Blob); + } } diff --git a/src/Resources/public/administration/js/aeon-dump-manager.js b/src/Resources/public/administration/js/aeon-dump-manager.js index 303c280..a98546c 100644 --- a/src/Resources/public/administration/js/aeon-dump-manager.js +++ b/src/Resources/public/administration/js/aeon-dump-manager.js @@ -1 +1 @@ -!function(){var e={74:function(){Shopware.Service("privileges").addPrivilegeMappingEntry({category:"permissions",parent:null,key:"aeon_dump_manager",roles:{viewer:{privileges:["aeon_dump_manager.viewer"],dependencies:[]},creator:{privileges:["aeon_dump_manager.creator"],dependencies:["aeon_dump_manager.viewer"]},deleter:{privileges:["aeon_dump_manager.deleter"],dependencies:["aeon_dump_manager.viewer"]}}})}},n={};function t(a){var i=n[a];if(void 0!==i)return i.exports;var r=n[a]={exports:{}};return e[a](r,r.exports,t),r.exports}t.p="bundles/aeondumpmanager/",window?.__sw__?.assetPath&&(t.p=window.__sw__.assetPath+"/bundles/aeondumpmanager/"),function(){"use strict";let{ApiService:e}=Shopware.Classes;class n extends e{constructor(e,n,t="aeon-dump-manager"){super(e,n,t)}getList(){return this.httpClient.get("/_action/aeon-dump-manager/dumps",{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}create(){return this.httpClient.post("/_action/aeon-dump-manager/dumps",{},{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}remove(n){return this.httpClient.delete(`/_action/aeon-dump-manager/dumps/${encodeURIComponent(n)}`,{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}getJobStatus(n){return this.httpClient.get(`/_action/aeon-dump-manager/dumps/jobs/${encodeURIComponent(n)}`,{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}}t(74);let{Component:a}=Shopware;a.register("aeon-dump-manager-list",{template:'{% block aeon_dump_manager_list %}\n\n \n\n \n\n \n\n{% endblock %}\n',inject:["aeonDumpManagerApiService","acl"],data(){return{dumps:[],isLoading:!1,pendingJob:null,deleteFilename:null,pollTimer:null}},computed:{columns(){return[{property:"datetime",label:this.$tc("aeon-dump-manager.list.columnDatetime"),allowResize:!0},{property:"filename",label:this.$tc("aeon-dump-manager.list.columnFilename"),allowResize:!0},{property:"sizeBytes",label:this.$tc("aeon-dump-manager.list.columnSize"),align:"right",allowResize:!0}]}},created(){this.loadList()},beforeDestroy(){this.stopPolling()},methods:{loadList(){return this.isLoading=!0,this.aeonDumpManagerApiService.getList().then(e=>{this.dumps=e.dumps}).finally(()=>{this.isLoading=!1})},formatDatetime(e){return new Date(e).toLocaleString()},formatSize(e){return Shopware.Utils.format.fileSize(e)},onCreate(){this.pendingJob={percent:0,status:"pending"},this.aeonDumpManagerApiService.create().then(e=>{this.pollJobStatus(e.jobId)}).catch(e=>{this.pendingJob=null,this.createNotificationError({message:this.$tc("aeon-dump-manager.list.createFailed",0,{error:e.message})})})},pollJobStatus(e){this.stopPolling(),this.pollTimer=window.setInterval(()=>{this.aeonDumpManagerApiService.getJobStatus(e).then(e=>{if("done"===e.status){this.stopPolling(),this.pendingJob=null,this.createNotificationSuccess({message:this.$tc("aeon-dump-manager.list.createSuccess")}),this.loadList();return}if("failed"===e.status){this.stopPolling(),this.pendingJob=null,this.createNotificationError({message:this.$tc("aeon-dump-manager.list.createFailed",0,{error:e.errorMessage??""})});return}this.pendingJob=e})},2e3)},stopPolling(){null!==this.pollTimer&&(window.clearInterval(this.pollTimer),this.pollTimer=null)},onRequestDelete(e){this.deleteFilename=e},onCancelDelete(){this.deleteFilename=null},onConfirmDelete(){let e=this.deleteFilename;return this.deleteFilename=null,this.aeonDumpManagerApiService.remove(e).then(()=>this.loadList())}}});var i=JSON.parse('{"aeon-dump-manager":{"general":{"mainMenuItemGeneral":"Dump-Verwaltung","descriptionTextModule":"SQL-Datenbank-Dumps erstellen, auflisten und l\xf6schen"},"list":{"columnFilename":"Name","columnDatetime":"Erstellt am","columnSize":"Gr\xf6\xdfe","buttonCreate":"Dump erstellen","buttonDelete":"L\xf6schen","confirmDeleteTitle":"Dump l\xf6schen?","confirmDeleteText":"M\xf6chten Sie \\"{filename}\\" wirklich l\xf6schen? Dies kann nicht r\xfcckg\xe4ngig gemacht werden.","empty":"Keine Dumps gefunden.","creating":"Dump wird erstellt…","createFailed":"Dump konnte nicht erstellt werden: {error}","createSuccess":"Dump erstellt."}}}'),r=JSON.parse('{"aeon-dump-manager":{"general":{"mainMenuItemGeneral":"Dump manager","descriptionTextModule":"Create, list and delete SQL database dumps"},"list":{"columnFilename":"Name","columnDatetime":"Created at","columnSize":"Size","buttonCreate":"Create dump","buttonDelete":"Delete","confirmDeleteTitle":"Delete dump?","confirmDeleteText":"Are you sure you want to delete \\"{filename}\\"? This cannot be undone.","empty":"No dumps found.","creating":"Creating dump…","createFailed":"Could not create dump: {error}","createSuccess":"Dump created."}}}');Shopware.Module.register("aeon-dump-manager",{type:"plugin",name:"DumpManager",title:"aeon-dump-manager.general.mainMenuItemGeneral",description:"aeon-dump-manager.general.descriptionTextModule",color:"#ff3d58",icon:"regular-database",snippets:{"de-DE":i,"en-GB":r},routes:{list:{component:"aeon-dump-manager-list",path:"list",meta:{parentPath:"sw.settings.index.system",privilege:"aeon_dump_manager.viewer"}}},navigation:[{id:"aeon-dump-manager-list",label:"aeon-dump-manager.general.mainMenuItemGeneral",color:"#ff3d58",path:"aeon.dump.manager.list",icon:"regular-database",parent:"sw-settings",position:100,privilege:"aeon_dump_manager.viewer"}],settingsItem:{group:"system",to:"aeon.dump.manager.list",icon:"regular-database",privilege:"aeon_dump_manager.viewer"}}),Shopware.Application.addServiceProvider("aeonDumpManagerApiService",e=>new n(Shopware.Application.getContainer("init").httpClient,e.loginService))}()}(); \ No newline at end of file +!function(){var e={74:function(){Shopware.Service("privileges").addPrivilegeMappingEntry({category:"permissions",parent:null,key:"aeon_dump_manager",roles:{viewer:{privileges:["aeon_dump_manager.viewer"],dependencies:[]},creator:{privileges:["aeon_dump_manager.creator"],dependencies:["aeon_dump_manager.viewer"]},deleter:{privileges:["aeon_dump_manager.deleter"],dependencies:["aeon_dump_manager.viewer"]}}})}},n={};function t(a){var l=n[a];if(void 0!==l)return l.exports;var i=n[a]={exports:{}};return e[a](i,i.exports,t),i.exports}t.p="bundles/aeondumpmanager/",window?.__sw__?.assetPath&&(t.p=window.__sw__.assetPath+"/bundles/aeondumpmanager/"),function(){"use strict";let{ApiService:e}=Shopware.Classes;class n extends e{constructor(e,n,t="aeon-dump-manager"){super(e,n,t)}getList(){return this.httpClient.get("/_action/aeon-dump-manager/dumps",{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}create(){return this.httpClient.post("/_action/aeon-dump-manager/dumps",{},{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}remove(n){return this.httpClient.delete(`/_action/aeon-dump-manager/dumps/${encodeURIComponent(n)}`,{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}getJobStatus(n){return this.httpClient.get(`/_action/aeon-dump-manager/dumps/jobs/${encodeURIComponent(n)}`,{headers:this.getBasicHeaders()}).then(n=>e.handleResponse(n))}download(n){return this.httpClient.get(`/_action/aeon-dump-manager/dumps/${encodeURIComponent(n)}/download`,{headers:this.getBasicHeaders(),responseType:"blob"}).then(n=>e.handleResponse(n))}}t(74);let{Component:a,Mixin:l}=Shopware;a.register("aeon-dump-manager-list",{template:'{% block aeon_dump_manager_list %}\n\n \n\n \n\n {% block aeon_dump_manager_list_progress_modal %}\n \n
\n \n \n \n
\n \n {% endblock %}\n\n \n
\n{% endblock %}\n',mixins:[l.getByName("notification")],inject:["aeonDumpManagerApiService","acl"],data(){return{dumps:[],isLoading:!1,pendingJob:null,deleteFilenames:null,selection:{},pollTimer:null}},computed:{columns(){return[{property:"datetime",label:this.$tc("aeon-dump-manager.list.columnDatetime"),allowResize:!0},{property:"filename",label:this.$tc("aeon-dump-manager.list.columnFilename"),allowResize:!0},{property:"sizeBytes",label:this.$tc("aeon-dump-manager.list.columnSize"),align:"right",allowResize:!0}]},selectionCount(){return Object.keys(this.selection).length}},created(){this.loadList()},beforeDestroy(){this.stopPolling()},methods:{loadList(){return this.isLoading=!0,this.aeonDumpManagerApiService.getList().then(e=>{this.dumps=e.dumps}).finally(()=>{this.isLoading=!1})},formatDatetime(e){return new Date(e).toLocaleString()},formatSize(e){return Shopware.Utils.format.fileSize(e)},onSelectionChange(e){this.selection=e},onCreate(){this.pendingJob={percent:0,status:"pending"},this.aeonDumpManagerApiService.create().then(e=>{this.pollJobStatus(e.jobId)}).catch(e=>{this.pendingJob={status:"failed",percent:null,errorMessage:e.message}})},pollJobStatus(e){this.stopPolling(),this.pollTimer=window.setInterval(()=>{this.aeonDumpManagerApiService.getJobStatus(e).then(e=>{if(this.pendingJob=e,"done"===e.status){this.stopPolling(),this.loadList(),window.setTimeout(()=>{this.pendingJob=null},1500);return}"failed"===e.status&&this.stopPolling()})},2e3)},stopPolling(){null!==this.pollTimer&&(window.clearInterval(this.pollTimer),this.pollTimer=null)},onCloseProgressModal(){this.stopPolling(),this.pendingJob=null},onDownload(e){this.aeonDumpManagerApiService.download(e).then(n=>{let t=window.URL.createObjectURL(n),a=document.createElement("a");a.href=t,a.download=e,document.body.appendChild(a),a.click(),document.body.removeChild(a),window.URL.revokeObjectURL(t)}).catch(e=>{this.createNotificationError({message:this.$tc("aeon-dump-manager.list.downloadFailed",0,{error:e.message})})})},onRequestDelete(e){this.deleteFilenames=[e]},onRequestBulkDelete(){this.deleteFilenames=Object.keys(this.selection)},onCancelDelete(){this.deleteFilenames=null},onConfirmDelete(){let e=this.deleteFilenames;return this.deleteFilenames=null,this.dumps=this.dumps.filter(n=>!e.includes(n.filename)),this.selection={},Promise.all(e.map(e=>this.aeonDumpManagerApiService.remove(e))).then(()=>this.loadList()).catch(e=>(this.createNotificationError({message:this.$tc("aeon-dump-manager.list.deleteFailed",0,{error:e.message})}),this.loadList()))}}});var i=JSON.parse('{"aeon-dump-manager":{"general":{"mainMenuItemGeneral":"Dump-Verwaltung","descriptionTextModule":"SQL-Datenbank-Dumps erstellen, auflisten und l\xf6schen"},"list":{"columnFilename":"Name","columnDatetime":"Erstellt am","columnSize":"Gr\xf6\xdfe","buttonCreate":"Dump erstellen","buttonDownload":"Herunterladen","buttonDelete":"L\xf6schen","bulkDeleteButton":"Auswahl l\xf6schen ({count})","confirmDeleteTitle":"Dump l\xf6schen?","confirmDeleteText":"M\xf6chten Sie \\"{filename}\\" wirklich l\xf6schen? Dies kann nicht r\xfcckg\xe4ngig gemacht werden.","confirmDeleteTextMultiple":"M\xf6chten Sie {count} Dumps wirklich l\xf6schen? Dies kann nicht r\xfcckg\xe4ngig gemacht werden.","empty":"Keine Dumps gefunden.","progressTitle":"Dump wird erstellt","progressText":"Dump wird erstellt…","createFailed":"Dump konnte nicht erstellt werden: {error}","createSuccess":"Dump erstellt.","deleteFailed":"Dump konnte nicht gel\xf6scht werden: {error}","downloadFailed":"Dump konnte nicht heruntergeladen werden: {error}"}}}'),o=JSON.parse('{"aeon-dump-manager":{"general":{"mainMenuItemGeneral":"Dump manager","descriptionTextModule":"Create, list and delete SQL database dumps"},"list":{"columnFilename":"Name","columnDatetime":"Created at","columnSize":"Size","buttonCreate":"Create dump","buttonDownload":"Download","buttonDelete":"Delete","bulkDeleteButton":"Delete selected ({count})","confirmDeleteTitle":"Delete dump?","confirmDeleteText":"Are you sure you want to delete \\"{filename}\\"? This cannot be undone.","confirmDeleteTextMultiple":"Are you sure you want to delete {count} dumps? This cannot be undone.","empty":"No dumps found.","progressTitle":"Creating dump","progressText":"Creating dump…","createFailed":"Could not create dump: {error}","createSuccess":"Dump created.","deleteFailed":"Could not delete dump: {error}","downloadFailed":"Could not download dump: {error}"}}}');Shopware.Module.register("aeon-dump-manager",{type:"plugin",name:"DumpManager",title:"aeon-dump-manager.general.mainMenuItemGeneral",description:"aeon-dump-manager.general.descriptionTextModule",color:"#ff3d58",icon:"regular-database",snippets:{"de-DE":i,"en-GB":o},routes:{list:{component:"aeon-dump-manager-list",path:"list",meta:{parentPath:"sw.settings.index.system",privilege:"aeon_dump_manager.viewer"}}},navigation:[{id:"aeon-dump-manager-list",label:"aeon-dump-manager.general.mainMenuItemGeneral",color:"#ff3d58",path:"aeon.dump.manager.list",icon:"regular-database",parent:"sw-settings",position:100,privilege:"aeon_dump_manager.viewer"}],settingsItem:{group:"system",to:"aeon.dump.manager.list",icon:"regular-database",privilege:"aeon_dump_manager.viewer"}}),Shopware.Application.addServiceProvider("aeonDumpManagerApiService",e=>new n(Shopware.Application.getContainer("init").httpClient,e.loginService))}()}(); \ No newline at end of file diff --git a/src/Service/DumpService.php b/src/Service/DumpService.php index 3e5dc99..8725be6 100644 --- a/src/Service/DumpService.php +++ b/src/Service/DumpService.php @@ -71,6 +71,23 @@ class DumpService $this->aeonDumpManagerFilesystemPrivate->delete($path); } + /** + * @return resource + */ + public function readStream(string $filename) + { + if (!$this->filenameParser->isValid($filename)) { + throw DumpNotFoundException::create($filename); + } + + $path = DumpLister::path($filename); + if (!$this->aeonDumpManagerFilesystemPrivate->fileExists($path)) { + throw DumpNotFoundException::create($filename); + } + + return $this->aeonDumpManagerFilesystemPrivate->readStream($path); + } + public function enforceMaxDumps(): void { $maxDumps = $this->systemConfigService->getInt('AeonDumpManager.config.maxDumps');