diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4fc9904..f622a50d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ This file is manually updated. Please file an issue if something is missing.
- **Feature**: Slideshow for photos and videos
- **Feature**: Support for GPU transcoding
+- **Feature**: Allow downloading entire albums
- Fixed support for HEVC live photos
- Fixed native photo sharing
- Use larger previews in viewer
diff --git a/appinfo/routes.php b/appinfo/routes.php
index ffb3fde3..83c755d2 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -45,6 +45,7 @@ return [
['name' => 'Days#dayPost', 'url' => '/api/days', 'verb' => 'POST'],
['name' => 'Albums#albums', 'url' => '/api/albums', 'verb' => 'GET'],
+ ['name' => 'Albums#download', 'url' => '/api/albums/download', 'verb' => 'POST'],
['name' => 'Tags#tags', 'url' => '/api/tags', 'verb' => 'GET'],
['name' => 'Tags#preview', 'url' => '/api/tags/preview/{tag}', 'verb' => 'GET'],
diff --git a/lib/Controller/AlbumsController.php b/lib/Controller/AlbumsController.php
index abd5ce97..4c08ec39 100644
--- a/lib/Controller/AlbumsController.php
+++ b/lib/Controller/AlbumsController.php
@@ -36,15 +36,10 @@ class AlbumsController extends ApiBase
public function albums(int $t = 0): JSONResponse
{
$user = $this->userSession->getUser();
- if (null === $user) {
+ if (null === $user || !$this->albumsIsEnabled()) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
}
- // Check tags enabled for this user
- if (!$this->albumsIsEnabled()) {
- return new JSONResponse(['message' => 'Albums not enabled for user'], Http::STATUS_PRECONDITION_FAILED);
- }
-
// Run actual query
$list = [];
if ($t & 1) { // personal
@@ -56,4 +51,34 @@ class AlbumsController extends ApiBase
return new JSONResponse($list, Http::STATUS_OK);
}
+
+ /**
+ * @NoAdminRequired
+ *
+ * Download an album as a zip file
+ */
+ public function download(string $name = ''): JSONResponse
+ {
+ $user = $this->userSession->getUser();
+ if (null === $user || !$this->albumsIsEnabled()) {
+ return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
+ }
+
+ // Get album
+ $album = $this->timelineQuery->getAlbumIfAllowed($user->getUID(), $name);
+ if (null === $album) {
+ return new JSONResponse([], Http::STATUS_NOT_FOUND);
+ }
+
+ // Get files
+ $files = $this->timelineQuery->getAlbumFiles($album['album_id']);
+ if (empty($files)) {
+ return new JSONResponse([], Http::STATUS_NOT_FOUND);
+ }
+
+ // Get download handle
+ $handle = \OCA\Memories\Controller\DownloadController::createHandle($files);
+
+ return new JSONResponse(['handle' => $handle], Http::STATUS_OK);
+ }
}
diff --git a/lib/Controller/DownloadController.php b/lib/Controller/DownloadController.php
index 9dc21838..24b1d7de 100644
--- a/lib/Controller/DownloadController.php
+++ b/lib/Controller/DownloadController.php
@@ -46,15 +46,21 @@ class DownloadController extends ApiBase
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
- // Store in session
- $session = \OC::$server->get(ISession::class);
-
- // Generate random id
- $handle = \OC::$server->get(ISecureRandom::class)->generate(16, ISecureRandom::CHAR_ALPHANUMERIC);
- $session->set("memories_download_ids_{$handle}", $files);
-
// Return id
- return new JSONResponse(['handle' => $handle]);
+ return new JSONResponse(['handle' => $this->createHandle($files)]);
+ }
+
+ /**
+ * Get a handle for downloading files.
+ *
+ * @param int[] $files
+ */
+ public static function createHandle(array $files): string
+ {
+ $handle = \OC::$server->get(ISecureRandom::class)->generate(16, ISecureRandom::CHAR_ALPHANUMERIC);
+ \OC::$server->get(ISession::class)->set("memories_download_ids_{$handle}", $files);
+
+ return $handle;
}
/**
@@ -153,12 +159,16 @@ class DownloadController extends ApiBase
/** @var ?File */
$file = null;
+ /** @var ?string */
+ $name = (string) $fileId;
+
try {
// This checks permissions
$file = $this->getUserFile($fileId);
if (null === $file) {
throw new \Exception('File not found');
}
+ $name = $file->getName();
// Open file
$handle = $file->fopen('rb');
@@ -167,9 +177,8 @@ class DownloadController extends ApiBase
}
// Handle duplicate names
- $name = $file->getName();
if (isset($nameCounts[$name])) {
- $nameCounts[$name] += 1;
+ ++$nameCounts[$name];
// add count before extension
$extpos = strrpos($name, '.');
@@ -192,9 +201,6 @@ class DownloadController extends ApiBase
throw new \Exception('Failed to add file to zip');
}
} catch (\Exception $e) {
- // Let the user know that something went wrong
- $name = $file->getName() ?: (string) $fileId;
-
// create a dummy memory file with the error message
$dummy = fopen('php://memory', 'rw+');
fwrite($dummy, $e->getMessage());
@@ -204,7 +210,7 @@ class DownloadController extends ApiBase
$dummy,
"{$name}_error.txt",
\strlen($e->getMessage()),
- $file->getMTime(),
+ time(),
);
// close the dummy file
diff --git a/lib/Db/TimelineQueryAlbums.php b/lib/Db/TimelineQueryAlbums.php
index f901ed90..7baa22e6 100644
--- a/lib/Db/TimelineQueryAlbums.php
+++ b/lib/Db/TimelineQueryAlbums.php
@@ -155,7 +155,7 @@ trait TimelineQueryAlbums
* @param string $uid UID of CURRENT user
* @param string $albumId $user/$name where $user is the OWNER of the album
*/
- private function getAlbumIfAllowed(string $uid, string $albumId)
+ public function getAlbumIfAllowed(string $uid, string $albumId)
{
// Split name and uid
$parts = explode('/', $albumId);
@@ -197,6 +197,26 @@ trait TimelineQueryAlbums
}
}
+ /**
+ * Get full list of fileIds in album.
+ */
+ public function getAlbumFiles(int $albumId)
+ {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('file_id')->from('photos_albums_files', 'paf')->where(
+ $query->expr()->eq('album_id', $query->createNamedParameter($albumId, IQueryBuilder::PARAM_INT))
+ );
+ $query->innerJoin('paf', 'filecache', 'fc', $query->expr()->eq('fc.fileid', 'paf.file_id'));
+
+ $fileIds = [];
+ $result = $query->executeQuery();
+ while ($row = $result->fetch()) {
+ $fileIds[] = (int) $row['file_id'];
+ }
+
+ return $fileIds;
+ }
+
/** Get the name of the collaborators table */
private function collaboratorsTable()
{
diff --git a/src/components/top-matter/AlbumTopMatter.vue b/src/components/top-matter/AlbumTopMatter.vue
index cc62dc39..bc4e6a38 100644
--- a/src/components/top-matter/AlbumTopMatter.vue
+++ b/src/components/top-matter/AlbumTopMatter.vue
@@ -29,6 +29,15 @@
{{ t("memories", "Share album") }}
+
+ {{ t("memories", "Download album") }}
+
+
diff --git a/src/services/API.ts b/src/services/API.ts
index 3af7d8fb..4b658df1 100644
--- a/src/services/API.ts
+++ b/src/services/API.ts
@@ -38,6 +38,10 @@ export class API {
return gen(`${BASE}/albums?t=${t}`);
}
+ static ALBUM_DOWNLOAD(user: string, name: string) {
+ return gen(`${BASE}/albums/download?name={user}/{name}`, { user, name });
+ }
+
static TAG_LIST() {
return gen(`${BASE}/tags`);
}
@@ -91,7 +95,7 @@ export class API {
return tok(gen(`${BASE}/download`));
}
- static DOWNLOAD_FILE(handle: number) {
+ static DOWNLOAD_FILE(handle: string) {
return tok(gen(`${BASE}/download/{handle}`, { handle }));
}
diff --git a/src/services/dav/download.ts b/src/services/dav/download.ts
index 727076e2..bd2df9af 100644
--- a/src/services/dav/download.ts
+++ b/src/services/dav/download.ts
@@ -18,7 +18,15 @@ export async function downloadFiles(fileIds: number[]) {
return;
}
- window.location.href = API.DOWNLOAD_FILE(res.data.handle);
+ downloadWithHandle(res.data.handle);
+}
+
+/**
+ * Download files with a download handle
+ * @param handle Download handle
+ */
+export function downloadWithHandle(handle: string) {
+ window.location.href = API.DOWNLOAD_FILE(handle);
}
/**