Skip to content
Permalink
Browse files Browse the repository at this point in the history
NEXT-15675 - Improve file download
  • Loading branch information
shyim authored and pweyck committed Aug 2, 2021
1 parent ec13f33 commit a9f52ab
Show file tree
Hide file tree
Showing 18 changed files with 243 additions and 42 deletions.
9 changes: 9 additions & 0 deletions changelog/_unreleased/2021-07-29-improve-file-download.md
@@ -0,0 +1,9 @@
---
title: Improve file download
issue: NEXT-15675
---
# Administration
* Replaced method `getDownloadUrl` with `openDownload` in components:
- `sw-import-export-activity`
- `sw-import-export-activity-detail-modal`
- `sw-import-export-progress`
Expand Up @@ -42,8 +42,17 @@ Shopware.Component.register('sw-import-export-activity-detail-modal', {
return format.fileSize(size);
},

getDownloadUrl(file) {
return this.importExport.getDownloadUrl(file.id, file.accessToken);
/**
* @deprecated tag:v6.5.0.0 - Remove unused method, use openDownload instead
*/
getDownloadUrl() {
Shopware.Utils.debug.error('The method getDownloadUrl has been replaced with openDownload.');

return '';
},

async openDownload(id) {
return window.open(await this.importExport.getDownloadUrl(id), '_blank');
},

getStateLabel(state) {
Expand Down
Expand Up @@ -84,7 +84,7 @@
variant="ghost"
class="sw-import-export-activity-detail-modal__download-failed"
size="small"
:link="getDownloadUrl(logEntity.invalidRecordsLog.file)"
@click="openDownload(logEntity.invalidRecordsLog.file)"
>
{{ $tc('sw-import-export.activity.detail.labelDownloadInvalidFile') }}
</sw-button>
Expand All @@ -107,8 +107,8 @@
</template>
{% block sw_import_export_edit_profile_modal_invalid_records_download %}
<a
:href="getDownloadUrl(logEntity.file)"
class="sw-import-export-activity-detail-modal__download"
@click="openDownload(logEntity.file)"
>
{{ $tc('sw-import-export.activity.detail.labelDownloadFile') }}
</a>
Expand All @@ -122,7 +122,7 @@
variant="ghost"
class="sw-import-export-activity-detail-modal__download-button"
size="small"
:link="getDownloadUrl(logEntity.file)"
@click="openDownload(logEntity.file)"
>
{{ $tc('sw-import-export.activity.detail.labelDownloadFile') }}
</sw-button>
Expand Down
Expand Up @@ -158,8 +158,17 @@ Shopware.Component.register('sw-import-export-activity', {
this.selectedLog = null;
},

getDownloadUrl(id, accessToken) {
return this.importExport.getDownloadUrl(id, accessToken);
/**
* @deprecated tag:v6.5.0.0 - Remove unused method, use openDownload instead
*/
getDownloadUrl() {
Shopware.Utils.debug.error('The method getDownloadUrl has been replaced with openDownload.');

return '';
},

async openDownload(id) {
return window.open(await this.importExport.getDownloadUrl(id), '_blank');
},

saveSelectedProfile() {
Expand Down
Expand Up @@ -80,9 +80,8 @@
{% endblock %}
{% block sw_import_export_activity_listing_actions_download_file %}
<a
class="sw-import-export-activity__download-action sw-external-link"
:href="getDownloadUrl(item.fileId, item.file.accessToken)"
target="_blank"
class="sw-import-export-activity__download-action"
@click="openDownload(item.fileId)"
>
<sw-context-menu-item>
{{ $tc('sw-import-export.activity.contextMenu.downloadFile') }}
Expand All @@ -97,9 +96,8 @@
{% block sw_import_export_activity_listing_actions_download_invalid %}
<template v-if="item.invalidRecordsLog">
<a
class="sw-import-export-activity__download-action sw-external-link"
:href="getDownloadUrl(item.invalidRecordsLog.fileId, item.invalidRecordsLog.file.accessToken)"
target="_blank"
class="sw-import-export-activity__download-action"
@click="openDownload(item.invalidRecordsLog.fileId)"
>
<sw-context-menu-item>
{{ $tc('sw-import-export.activity.contextMenu.downloadInvalidFile') }}
Expand Down
Expand Up @@ -131,8 +131,17 @@ Shopware.Component.register('sw-import-export-progress', {
},

methods: {
getDownloadUrl(id, accessToken) {
return this.importExport.getDownloadUrl(id, accessToken);
/**
* @deprecated tag:v6.5.0.0 - Remove unused method, use openDownload instead
*/
getDownloadUrl() {
Shopware.Utils.debug.error('The method getDownloadUrl has been replaced with openDownload.');

return '';
},

async openDownload(id) {
return window.open(await this.importExport.getDownloadUrl(id), '_blank');
},

onShowLog(item) {
Expand Down
Expand Up @@ -110,10 +110,7 @@
<sw-button variant="primary"
size="small"
class="sw-import-export-progress__stats-list-failure-download"
:link="getDownloadUrl(
logEntry.invalidRecordsLog.file.id,
logEntry.invalidRecordsLog.file.accessToken
)">
@click="openDownload(logEntry.invalidRecordsLog.file.id)">
{{ $tc('sw-import-export.progress.downloadFailedLabel') }}
</sw-button>
</template>
Expand All @@ -125,7 +122,7 @@
variant="primary"
class="sw-import-export-progress__download-action"
size="small"
:link="getDownloadUrl(logEntry.file.id, logEntry.file.accessToken)"
@click="openDownload(logEntry.file.id)"
>
{{ $tc('sw-import-export.progress.downloadFileLabel') }}
</sw-button>
Expand Down
Expand Up @@ -36,12 +36,17 @@ export default class ImportExportService extends ApiService {
* Download the export file
*
* @param fileId {Entity} File entity
* @param accessToken
* @returns {string}
*/
getDownloadUrl(fileId, accessToken) {
async getDownloadUrl(fileId) {
const accessTokenResponse = await this.httpClient.post(
`/_action/import-export/file/prepare-download/${fileId}`,
{},
{ headers: this.getBasicHeaders() },
);

return `${Shopware.Context.api.apiPath}/_action/${this.getApiBasePath()}/` +
`file/download?fileId=${fileId}&accessToken=${accessToken}`;
`file/download?fileId=${fileId}&accessToken=${accessTokenResponse.data.accessToken}`;
}

/**
Expand Down
Expand Up @@ -20,10 +20,10 @@

{% block sw_import_export_view_profile_profiles_toolbar_add_new_profile %}
<sw-button
v-tooltip="createTooltip"
class="sw-import-export-view-profiles__create-action"
variant="ghost"
:disabled="isLoading || isNotSystemLanguage"
v-tooltip="createTooltip"
size="small"
@click="onAddNewProfile"
>
Expand Down
Expand Up @@ -41,7 +41,7 @@ protected function defineFields(): FieldCollection
(new DateTimeField('expire_date', 'expireDate'))->addFlags(new Required()),
new IntField('size', 'size'),
new OneToOneAssociationField('log', 'id', 'file_id', ImportExportLogDefinition::class, false),
(new StringField('access_token', 'accessToken'))->addFlags(new Required()),
new StringField('access_token', 'accessToken'),
]);
}
}
Expand Up @@ -37,7 +37,7 @@ class ImportExportFileEntity extends Entity
protected $log;

/**
* @var string
* @var string|null
*/
protected $accessToken;

Expand Down Expand Up @@ -91,12 +91,12 @@ public function setLog(ImportExportLogEntity $log): void
$this->log = $log;
}

public function getAccessToken(): string
public function getAccessToken(): ?string
{
return $this->accessToken;
}

public function setAccessToken(string $accessToken): void
public function setAccessToken(?string $accessToken): void
{
$this->accessToken = $accessToken;
}
Expand Down
Expand Up @@ -154,6 +154,17 @@ public function process(Request $request, Context $context): JsonResponse
return new JsonResponse(['progress' => $progress->jsonSerialize()]);
}

/**
* @Since("6.4.3.1")
* @Route("/api/_action/import-export/file/prepare-download/{fileId}", name="api.action.import_export.file.prepare-download", methods={"POST"})
*/
public function prepareDownload(string $fileId, Context $context): Response
{
$token = $this->downloadService->regenerateToken($context, $fileId);

return new JsonResponse(['accessToken' => $token]);
}

/**
* @Since("6.0.0.0")
* @Route("/api/_action/import-export/file/download", name="api.action.import_export.file.download", defaults={"auth_required"=false}, methods={"GET"})
Expand Down
39 changes: 28 additions & 11 deletions src/Core/Content/ImportExport/Service/DownloadService.php
Expand Up @@ -18,37 +18,43 @@
*/
class DownloadService
{
/**
* @var FilesystemInterface
*/
private $filesystem;
private FilesystemInterface $filesystem;

/**
* @var EntityRepositoryInterface
*/
private $fileRepository;
private EntityRepositoryInterface $fileRepository;

public function __construct(FilesystemInterface $filesystem, EntityRepositoryInterface $fileRepository)
{
$this->filesystem = $filesystem;
$this->fileRepository = $fileRepository;
}

public function regenerateToken(Context $context, string $fileId): void
public function regenerateToken(Context $context, string $fileId): string
{
$token = ImportExportFileEntity::generateAccessToken();

$this->fileRepository->update(
[['id' => $fileId, 'accessToken' => ImportExportFileEntity::generateAccessToken()]],
[['id' => $fileId, 'accessToken' => $token]],
$context
);

return $token;
}

public function createFileResponse(Context $context, string $fileId, string $accessToken): Response
{
$entity = $this->findFile($context, $fileId);
if ($entity->getAccessToken() !== $accessToken) {

$fileAccessToken = (string) $entity->getAccessToken();

if ($fileAccessToken === '' || $entity->getAccessToken() !== $accessToken || !$this->isModifiedRecently($entity)) {
throw new InvalidFileAccessTokenException();
}

$this->fileRepository->update(
[['id' => $fileId, 'accessToken' => null]],
$context
);

$headers = [
'Content-Disposition' => HeaderUtils::makeDisposition(
'attachment',
Expand Down Expand Up @@ -78,4 +84,15 @@ private function findFile(Context $context, string $fileId): ImportExportFileEnt

return $entity;
}

private function isModifiedRecently(ImportExportFileEntity $entity): bool
{
if ($entity->getUpdatedAt() === null) {
return false;
}

$diff = time() - $entity->getUpdatedAt()->getTimestamp();

return $diff < 300;
}
}
Expand Up @@ -219,7 +219,7 @@ private function storeFile(Context $context, \DateTimeInterface $expireDate, ?st
'path' => $path,
'size' => $this->filesystem->getSize($path),
'expireDate' => $expireDate,
'accessToken' => ImportExportFileEntity::generateAccessToken(),
'accessToken' => null,
];

$this->fileRepository->create([$fileData], $context);
Expand Down

0 comments on commit a9f52ab

Please sign in to comment.