albums: add download menu
parent
b2ad076c06
commit
2011433536
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -29,6 +29,15 @@
|
|||
{{ t("memories", "Share album") }}
|
||||
<template #icon> <ShareIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Download album')"
|
||||
@click="downloadAlbum()"
|
||||
close-after-click
|
||||
v-if="!isAlbumList"
|
||||
>
|
||||
{{ t("memories", "Download album") }}
|
||||
<template #icon> <DownloadIcon :size="20" /> </template>
|
||||
</NcActionButton>
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Edit album details')"
|
||||
@click="$refs.createModal.open(true)"
|
||||
|
@ -65,16 +74,21 @@ import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
|||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox";
|
||||
import { getCurrentUser } from "@nextcloud/auth";
|
||||
import axios from "@nextcloud/axios";
|
||||
|
||||
import AlbumCreateModal from "../modal/AlbumCreateModal.vue";
|
||||
import AlbumDeleteModal from "../modal/AlbumDeleteModal.vue";
|
||||
import AlbumShareModal from "../modal/AlbumShareModal.vue";
|
||||
|
||||
import { downloadWithHandle } from "../../services/dav/download";
|
||||
|
||||
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
|
||||
import DownloadIcon from "vue-material-design-icons/Download.vue";
|
||||
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
||||
import DeleteIcon from "vue-material-design-icons/Close.vue";
|
||||
import PlusIcon from "vue-material-design-icons/Plus.vue";
|
||||
import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
||||
import { API } from "../../services/API";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
|
@ -87,6 +101,7 @@ import ShareIcon from "vue-material-design-icons/ShareVariant.vue";
|
|||
AlbumShareModal,
|
||||
|
||||
BackIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
DeleteIcon,
|
||||
PlusIcon,
|
||||
|
@ -122,6 +137,15 @@ export default class AlbumTopMatter extends Mixins(GlobalMixin, UserConfig) {
|
|||
back() {
|
||||
this.$router.push({ name: "albums" });
|
||||
}
|
||||
|
||||
async downloadAlbum() {
|
||||
const res = await axios.post(
|
||||
API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name)
|
||||
);
|
||||
if (res.status === 200 && res.data.handle) {
|
||||
downloadWithHandle(res.data.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue