Skip to content

Commit 73f80bd

Browse files
authored
Merge pull request #512 from nextcloud/backport/510/stable31
[stable31] fix: keep track of download count
2 parents dc57647 + e5fdf99 commit 73f80bd

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\DAV\Events\SabrePluginAddEvent;
1313
use OCA\Files\Event\LoadSidebar;
1414
use OCA\Files_DownloadLimit\Capabilities;
15+
use OCA\Files_DownloadLimit\Listener\BeforeNodeReadListener;
1516
use OCA\Files_DownloadLimit\Listener\BeforeTemplateRenderedListener;
1617
use OCA\Files_DownloadLimit\Listener\LoadSidebarListener;
1718
use OCA\Files_DownloadLimit\Listener\SabrePluginAddListener;
@@ -22,6 +23,7 @@
2223
use OCP\AppFramework\Bootstrap\IBootContext;
2324
use OCP\AppFramework\Bootstrap\IBootstrap;
2425
use OCP\AppFramework\Bootstrap\IRegistrationContext;
26+
use OCP\Files\Events\Node\BeforeNodeReadEvent;
2527

2628
class Application extends App implements IBootstrap {
2729
public const APP_ID = 'files_downloadlimit';
@@ -36,6 +38,8 @@ public function register(IRegistrationContext $context): void {
3638
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
3739
$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
3840
$context->registerEventListener(ShareLinkAccessedEvent::class, ShareLinkAccessedListener::class);
41+
$context->registerEventListener(BeforeNodeReadEvent::class, BeforeNodeReadListener::class);
42+
3943

4044
$context->registerCapability(Capabilities::class);
4145
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files_DownloadLimit\Listener;
11+
12+
use OCA\Files_DownloadLimit\Db\LimitMapper;
13+
use OCP\AppFramework\Db\DoesNotExistException;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
use OCP\Files\Events\BeforeZipCreatedEvent;
17+
use OCP\Files\Events\Node\BeforeNodeReadEvent;
18+
use OCP\Files\File;
19+
use OCP\Files\Folder;
20+
use OCP\Files\NotFoundException;
21+
use OCP\Files\Storage\ISharedStorage;
22+
use OCP\ICache;
23+
use OCP\ICacheFactory;
24+
use OCP\IRequest;
25+
use OCP\ISession;
26+
use OCP\Share\IManager;
27+
use OCP\Share\IShare;
28+
use Psr\Log\LoggerInterface;
29+
30+
/**
31+
* @template-implements IEventListener<BeforeNodeReadEvent|BeforeZipCreatedEvent|Event>
32+
*/
33+
class BeforeNodeReadListener implements IEventListener {
34+
private ICache $cache;
35+
public function __construct(
36+
private IManager $manager,
37+
private LimitMapper $mapper,
38+
private LoggerInterface $logger,
39+
private IRequest $request,
40+
private ISession $session,
41+
ICacheFactory $cacheFactory,
42+
) {
43+
$this->cache = $cacheFactory->createDistributed('files_downloadlimit_event');
44+
}
45+
46+
public function handle(Event $event): void {
47+
if ($event instanceof BeforeZipCreatedEvent) {
48+
$this->handleBeforeZipCreatedEvent($event);
49+
} elseif ($event instanceof BeforeNodeReadEvent) {
50+
$this->handleBeforeNodeReadEvent($event);
51+
}
52+
}
53+
54+
public function handleBeforeZipCreatedEvent(BeforeZipCreatedEvent $event): void {
55+
$files = $event->getFiles();
56+
if (count($files) !== 0) {
57+
/* No need to do anything, count will be triggered for each file in the zip by the BeforeNodeReadEvent */
58+
return;
59+
}
60+
61+
$node = $event->getFolder();
62+
if (!($node instanceof Folder)) {
63+
return;
64+
}
65+
66+
try {
67+
$storage = $node->getStorage();
68+
} catch (NotFoundException) {
69+
return;
70+
}
71+
72+
if (!$storage->instanceOfStorage(ISharedStorage::class)) {
73+
return;
74+
}
75+
76+
/** @var ISharedStorage $storage */
77+
$share = $storage->getShare();
78+
79+
if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) {
80+
return;
81+
}
82+
83+
/* Cache that that folder download activity was published */
84+
$this->cache->set($this->request->getId(), $node->getPath(), 3600);
85+
86+
$this->singleFileDownloaded($share);
87+
}
88+
89+
public function handleBeforeNodeReadEvent(BeforeNodeReadEvent $event): void {
90+
$node = $event->getNode();
91+
if (!($node instanceof File)) {
92+
return;
93+
}
94+
95+
try {
96+
$storage = $node->getStorage();
97+
} catch (NotFoundException) {
98+
return;
99+
}
100+
101+
if (!$storage->instanceOfStorage(ISharedStorage::class)) {
102+
return;
103+
}
104+
105+
/** @var ISharedStorage $storage */
106+
$share = $storage->getShare();
107+
108+
if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) {
109+
return;
110+
}
111+
112+
$path = $this->cache->get($this->request->getId());
113+
if (is_string($path) && str_starts_with($node->getPath(), $path)) {
114+
/* An activity was published for a containing folder already */
115+
return;
116+
}
117+
118+
/* Avoid publishing several activities for one video playing */
119+
$cacheKey = $node->getId() . $node->getPath() . $this->session->getId();
120+
if (($this->request->getHeader('range') !== '') && ($this->cache->get($cacheKey) === 'true')) {
121+
/* This is a range request and an activity for the same file was published in the same session */
122+
return;
123+
}
124+
$this->cache->set($cacheKey, 'true', 3600);
125+
126+
127+
$this->singleFileDownloaded($share);
128+
}
129+
130+
protected function singleFileDownloaded(IShare $share): void {
131+
132+
$token = $share->getToken();
133+
if ($token === null) {
134+
return;
135+
}
136+
// Make sure we have a valid limit
137+
try {
138+
$shareLimit = $this->mapper->get($token);
139+
$limit = $shareLimit->getLimit();
140+
141+
// Increment this download event
142+
$downloads = $shareLimit->getDownloads() + 1;
143+
144+
// If we reached the maximum allowed download count
145+
if ($downloads >= $limit) {
146+
// Delete share
147+
$this->manager->deleteShare($share);
148+
// Delete limit
149+
$this->mapper->delete($shareLimit);
150+
return;
151+
}
152+
153+
// Else, we just update the current download count
154+
$shareLimit->setDownloads($downloads);
155+
$this->mapper->update($shareLimit);
156+
} catch (DoesNotExistException $e) {
157+
// No limit is set, ignore
158+
} catch (\Exception $e) {
159+
$this->logger->error('Error while handling share link accessed event: ' . $e->getMessage());
160+
}
161+
162+
163+
}
164+
165+
}

0 commit comments

Comments
 (0)