folder-share: add creation button
parent
b94e055abc
commit
1891d86f63
|
@ -84,6 +84,10 @@ return [
|
|||
['name' => 'Download#file', 'url' => '/api/download/{handle}', 'verb' => 'GET'],
|
||||
['name' => 'Download#one', 'url' => '/api/stream/{fileid}', 'verb' => 'GET'],
|
||||
|
||||
['name' => 'Share#links', 'url' => '/api/share/links', 'verb' => 'GET'],
|
||||
['name' => 'Share#createNode', 'url' => '/api/share/node', 'verb' => 'POST'],
|
||||
['name' => 'Share#deleteShare', 'url' => '/api/share/delete', 'verb' => 'POST'],
|
||||
|
||||
// Config API
|
||||
['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
|
||||
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 Varun Patil <radialapps@gmail.com>
|
||||
* @author Varun Patil <radialapps@gmail.com>
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Controller;
|
||||
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
|
||||
class ShareController extends ApiBase
|
||||
{
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* Get the tokens of a node shared using an external link
|
||||
*/
|
||||
public function links($id, $path)
|
||||
{
|
||||
$file = $this->getNodeByIdOrPath($id, $path);
|
||||
if (!$file) {
|
||||
return new JSONResponse([
|
||||
'message' => 'File not found',
|
||||
], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
/** @var \OCP\Share\IManager $shareManager */
|
||||
$shareManager = \OC::$server->get(\OCP\Share\IManager::class);
|
||||
|
||||
$shares = $shareManager->getSharesBy($this->getUID(), \OCP\Share\IShare::TYPE_LINK, $file, true, 50, 0);
|
||||
if (empty($shares)) {
|
||||
return new JSONResponse([
|
||||
'message' => 'No external links found',
|
||||
], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
/** @var \OCP\IURLGenerator $urlGenerator */
|
||||
$urlGenerator = \OC::$server->get(\OCP\IURLGenerator::class);
|
||||
|
||||
$links = array_map(function (\OCP\Share\IShare $share) use ($urlGenerator) {
|
||||
$tok = $share->getToken();
|
||||
$exp = $share->getExpirationDate();
|
||||
$url = $urlGenerator->linkToRouteAbsolute('memories.Public.showShare', [
|
||||
'token' => $tok,
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $share->getFullId(),
|
||||
'label' => $share->getLabel(),
|
||||
'token' => $tok,
|
||||
'url' => $url,
|
||||
'hasPassword' => $share->getPassword() ? true : false,
|
||||
'expiration' => $exp ? $exp->getTimestamp() : null,
|
||||
'editable' => $share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE,
|
||||
];
|
||||
}, $shares);
|
||||
|
||||
return new JSONResponse($links, Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* Share a node using an external link
|
||||
*/
|
||||
public function createNode($id, $path)
|
||||
{
|
||||
$file = $this->getNodeByIdOrPath($id, $path);
|
||||
if (!$file) {
|
||||
return new JSONResponse([
|
||||
'message' => 'You are not allowed to share this file',
|
||||
], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
/** @var \OCP\Share\IManager $shareManager */
|
||||
$shareManager = \OC::$server->get(\OCP\Share\IManager::class);
|
||||
|
||||
/** @var \OCP\Share\IShare $share */
|
||||
$share = $shareManager->newShare();
|
||||
$share->setNode($file);
|
||||
$share->setShareType(\OCP\Share\IShare::TYPE_LINK);
|
||||
$share->setSharedBy($this->userSession->getUser()->getUID());
|
||||
$share->setPermissions(\OCP\Constants::PERMISSION_READ);
|
||||
|
||||
$shareManager->createShare($share);
|
||||
|
||||
return new JSONResponse([
|
||||
'token' => $share->getToken(),
|
||||
], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* Delete an external link share
|
||||
*/
|
||||
public function deleteShare(string $id)
|
||||
{
|
||||
$uid = $this->getUID();
|
||||
if (!$uid) {
|
||||
return new JSONResponse([
|
||||
'message' => 'You are not logged in',
|
||||
], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
/** @var \OCP\Share\IManager $shareManager */
|
||||
$shareManager = \OC::$server->get(\OCP\Share\IManager::class);
|
||||
|
||||
$share = $shareManager->getShareById($id);
|
||||
|
||||
if ($share->getSharedBy() !== $uid) {
|
||||
return new JSONResponse([
|
||||
'message' => 'You are not the owner of this share',
|
||||
], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$shareManager->deleteShare($share);
|
||||
|
||||
return new JSONResponse([], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
private function getNodeByIdOrPath($id, $path) {
|
||||
$uid = $this->getUID();
|
||||
if (!$uid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = null;
|
||||
if ($id) {
|
||||
$file = $this->getUserFile($id);
|
||||
} else if ($path) {
|
||||
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||
$file = $userFolder->get($path);
|
||||
}
|
||||
|
||||
if (!$file || !$file->isShareable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
}
|
|
@ -6,36 +6,65 @@
|
|||
:sidebar="!isRoot ? this.folderPath : null"
|
||||
>
|
||||
<template #title>
|
||||
{{ t("memories", "Share Folder") }}
|
||||
{{ t("memories", "Link Sharing") }}
|
||||
</template>
|
||||
|
||||
<div v-if="isRoot">
|
||||
{{ t("memories", "You cannot share the root folder") }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ t("memories", "Use the sidebar to share this folder.") }} <br />
|
||||
{{
|
||||
t(
|
||||
"memories",
|
||||
"After creating a public share link in the sidebar, click Refresh and a corresponding link to Memories will be shown below."
|
||||
"Public link shares are available to people outside Nextcloud."
|
||||
)
|
||||
}}
|
||||
<br />
|
||||
{{
|
||||
t(
|
||||
"memories",
|
||||
"You may create or update permissions on public links using the sidebar."
|
||||
)
|
||||
}}
|
||||
<br />
|
||||
{{ t("memories", "Click a link to copy to clipboard.") }}
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a
|
||||
v-for="link of links"
|
||||
:key="link.url"
|
||||
:href="link.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ link.url }}
|
||||
</a>
|
||||
<ul>
|
||||
<NcListItem
|
||||
v-for="share of shares"
|
||||
:title="share.label || share.token"
|
||||
:key="share.id"
|
||||
:bold="false"
|
||||
@click="copy(share.url)"
|
||||
>
|
||||
<template #icon>
|
||||
<LinkIcon class="avatar" :size="20" />
|
||||
</template>
|
||||
<template #subtitle>
|
||||
{{ getShareLabels(share) }}
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton @click="deleteLink(share)" :disabled="loading">
|
||||
{{ t("memories", "Remove") }}
|
||||
|
||||
<template #icon>
|
||||
<CloseIcon :size="20" />
|
||||
</template>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<NcLoadingIcon v-if="loading" />
|
||||
|
||||
<template #buttons>
|
||||
<NcButton class="primary" @click="refreshUrls">
|
||||
<NcButton class="primary" :disabled="loading" @click="createLink">
|
||||
{{ t("memories", "Create Link") }}
|
||||
</NcButton>
|
||||
<NcButton class="primary" :disabled="loading" @click="refreshUrls">
|
||||
{{ t("memories", "Refresh") }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
@ -46,20 +75,44 @@
|
|||
import { defineComponent } from "vue";
|
||||
|
||||
import axios from "@nextcloud/axios";
|
||||
import { generateOcsUrl, generateUrl } from "@nextcloud/router";
|
||||
import { showSuccess } from "@nextcloud/dialogs";
|
||||
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
|
||||
|
||||
import UserConfig from "../../mixins/UserConfig";
|
||||
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
|
||||
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
||||
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem");
|
||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||
|
||||
import * as utils from "../../services/Utils";
|
||||
import Modal from "./Modal.vue";
|
||||
import { Type } from "@nextcloud/sharing";
|
||||
|
||||
import { API } from "../../services/API";
|
||||
|
||||
import CloseIcon from "vue-material-design-icons/Close.vue";
|
||||
import LinkIcon from "vue-material-design-icons/LinkVariant.vue";
|
||||
|
||||
type IShare = {
|
||||
id: string;
|
||||
label: string;
|
||||
token: string;
|
||||
url: string;
|
||||
hasPassword: boolean;
|
||||
expiration: number | null;
|
||||
editable: number;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "FolderShareModal",
|
||||
components: {
|
||||
Modal,
|
||||
NcButton,
|
||||
NcLoadingIcon,
|
||||
NcListItem,
|
||||
NcActionButton,
|
||||
|
||||
CloseIcon,
|
||||
LinkIcon,
|
||||
},
|
||||
|
||||
mixins: [UserConfig],
|
||||
|
@ -67,7 +120,8 @@ export default defineComponent({
|
|||
data: () => ({
|
||||
show: false,
|
||||
folderPath: "",
|
||||
links: [] as { url: string }[],
|
||||
loading: false,
|
||||
shares: [] as IShare[],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
|
@ -76,6 +130,14 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
subscribe("update:share", this.refreshUrls);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
unsubscribe("update:share", this.refreshUrls);
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
|
@ -90,22 +152,63 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async refreshUrls() {
|
||||
const query = `format=json&path=${encodeURIComponent(
|
||||
this.folderPath
|
||||
)}&reshares=true`;
|
||||
const url = generateOcsUrl(`/apps/files_sharing/api/v1/shares?${query}`);
|
||||
const response = await axios.get(url);
|
||||
const data = response.data?.ocs?.data;
|
||||
if (data) {
|
||||
this.links = data
|
||||
.filter((s) => s.share_type === Type.SHARE_TYPE_LINK && s.token)
|
||||
.map((share: any) => ({
|
||||
url:
|
||||
window.location.origin +
|
||||
generateUrl(`/apps/memories/s/${share.token}`),
|
||||
}));
|
||||
this.loading = true;
|
||||
try {
|
||||
this.shares = (
|
||||
await axios.get(API.Q(API.SHARE_LINKS(), { path: this.folderPath }))
|
||||
).data;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getShareLabels(share: IShare): string {
|
||||
const labels = [];
|
||||
if (share.hasPassword) {
|
||||
labels.push(this.t("memories", "Password protected"));
|
||||
}
|
||||
|
||||
if (share.expiration) {
|
||||
const exp = utils.getLongDateStr(new Date(share.expiration * 1000));
|
||||
const kw = this.t("memories", "Expires");
|
||||
labels.push(`${kw} ${exp}`);
|
||||
}
|
||||
|
||||
if (share.editable) {
|
||||
labels.push(this.t("memories", "Editable"));
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
return `${labels.join(", ")}`;
|
||||
}
|
||||
|
||||
return this.t("memories", "Read only");
|
||||
},
|
||||
|
||||
async createLink() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await axios.post(API.SHARE_NODE(), { path: this.folderPath });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
this.refreshUrls();
|
||||
},
|
||||
|
||||
async deleteLink(share: IShare) {
|
||||
this.loading = true;
|
||||
try {
|
||||
await axios.post(API.SHARE_DELETE(), { id: share.id });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
this.refreshUrls();
|
||||
},
|
||||
|
||||
copy(url: string) {
|
||||
window.navigator.clipboard.writeText(url);
|
||||
showSuccess(this.t("memories", "Link copied to clipboard"));
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -113,10 +216,9 @@ export default defineComponent({
|
|||
<style lang="scss" scoped>
|
||||
.links {
|
||||
margin-top: 1em;
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: 0.2em;
|
||||
color: var(--color-primary-element);
|
||||
|
||||
:deep .avatar {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,16 +18,24 @@ function tok(url: string) {
|
|||
}
|
||||
|
||||
export class API {
|
||||
static Q(url: string, query: string | URLSearchParams | undefined | null) {
|
||||
static Q(
|
||||
url: string,
|
||||
query: string | URLSearchParams | Object | undefined | null
|
||||
) {
|
||||
if (!query) return url;
|
||||
|
||||
let queryStr = typeof query === "string" ? query : query.toString();
|
||||
if (!queryStr) return url;
|
||||
if (query instanceof URLSearchParams) {
|
||||
query = query.toString();
|
||||
} else if (typeof query === "object") {
|
||||
query = new URLSearchParams(query as any).toString();
|
||||
}
|
||||
|
||||
if (!query) return url;
|
||||
|
||||
if (url.indexOf("?") > -1) {
|
||||
return `${url}&${queryStr}`;
|
||||
return `${url}&${query}`;
|
||||
} else {
|
||||
return `${url}?${queryStr}`;
|
||||
return `${url}?${query}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,6 +135,18 @@ export class API {
|
|||
return tok(gen(`${BASE}/stream/{id}`, { id }));
|
||||
}
|
||||
|
||||
static SHARE_LINKS() {
|
||||
return gen(`${BASE}/share/links`);
|
||||
}
|
||||
|
||||
static SHARE_NODE() {
|
||||
return gen(`${BASE}/share/node`);
|
||||
}
|
||||
|
||||
static SHARE_DELETE() {
|
||||
return gen(`${BASE}/share/delete`);
|
||||
}
|
||||
|
||||
static CONFIG(setting: string) {
|
||||
return gen(`${BASE}/config/{setting}`, { setting });
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue