1
0

QuotaPlugin.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-FileCopyrightText: 2012 entreCables S.L. All rights reserved
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OCA\DAV\Connector\Sabre;
  9. use OCA\DAV\Upload\FutureFile;
  10. use OCA\DAV\Upload\UploadFolder;
  11. use OCP\Files\StorageNotAvailableException;
  12. use Sabre\DAV\Exception\InsufficientStorage;
  13. use Sabre\DAV\Exception\ServiceUnavailable;
  14. use Sabre\DAV\INode;
  15. /**
  16. * This plugin check user quota and deny creating files when they exceeds the quota.
  17. *
  18. * @author Sergio Cambra
  19. * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
  20. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  21. */
  22. class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
  23. /** @var \OC\Files\View */
  24. private $view;
  25. /**
  26. * Reference to main server object
  27. *
  28. * @var \Sabre\DAV\Server
  29. */
  30. private $server;
  31. /**
  32. * @param \OC\Files\View $view
  33. */
  34. public function __construct($view) {
  35. $this->view = $view;
  36. }
  37. /**
  38. * This initializes the plugin.
  39. *
  40. * This function is called by \Sabre\DAV\Server, after
  41. * addPlugin is called.
  42. *
  43. * This method should set up the requires event subscriptions.
  44. *
  45. * @param \Sabre\DAV\Server $server
  46. * @return void
  47. */
  48. public function initialize(\Sabre\DAV\Server $server) {
  49. $this->server = $server;
  50. $server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
  51. $server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
  52. $server->on('beforeMove', [$this, 'beforeMove'], 10);
  53. $server->on('beforeCopy', [$this, 'beforeCopy'], 10);
  54. }
  55. /**
  56. * Check quota before creating file
  57. *
  58. * @param string $uri target file URI
  59. * @param resource $data data
  60. * @param INode $parent Sabre Node
  61. * @param bool $modified modified
  62. */
  63. public function beforeCreateFile($uri, $data, INode $parent, $modified) {
  64. $request = $this->server->httpRequest;
  65. if ($parent instanceof UploadFolder && $request->getHeader('Destination')) {
  66. // If chunked upload and Total-Length header is set, use that
  67. // value for quota check. This allows us to also check quota while
  68. // uploading chunks and not only when the file is assembled.
  69. $length = $request->getHeader('OC-Total-Length');
  70. $destinationPath = $this->server->calculateUri($request->getHeader('Destination'));
  71. $quotaPath = $this->getPathForDestination($destinationPath);
  72. if ($quotaPath && is_numeric($length)) {
  73. return $this->checkQuota($quotaPath, (int)$length);
  74. }
  75. }
  76. if (!$parent instanceof Node) {
  77. return;
  78. }
  79. return $this->checkQuota($parent->getPath() . '/' . basename($uri));
  80. }
  81. /**
  82. * Check quota before writing content
  83. *
  84. * @param string $uri target file URI
  85. * @param INode $node Sabre Node
  86. * @param resource $data data
  87. * @param bool $modified modified
  88. */
  89. public function beforeWriteContent($uri, INode $node, $data, $modified) {
  90. if (!$node instanceof Node) {
  91. return;
  92. }
  93. return $this->checkQuota($node->getPath());
  94. }
  95. /**
  96. * Check if we're moving a FutureFile in which case we need to check
  97. * the quota on the target destination.
  98. */
  99. public function beforeMove(string $sourcePath, string $destinationPath): bool {
  100. $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
  101. if (!$sourceNode instanceof FutureFile) {
  102. return true;
  103. }
  104. try {
  105. // The final path is not known yet, we check the quota on the parent
  106. $path = $this->getPathForDestination($destinationPath);
  107. } catch (\Exception $e) {
  108. return true;
  109. }
  110. return $this->checkQuota($path, $sourceNode->getSize());
  111. }
  112. /**
  113. * Check quota on the target destination before a copy.
  114. */
  115. public function beforeCopy(string $sourcePath, string $destinationPath): bool {
  116. $sourceNode = $this->server->tree->getNodeForPath($sourcePath);
  117. if (!$sourceNode instanceof Node) {
  118. return true;
  119. }
  120. try {
  121. $path = $this->getPathForDestination($destinationPath);
  122. } catch (\Exception $e) {
  123. return true;
  124. }
  125. return $this->checkQuota($path, $sourceNode->getSize());
  126. }
  127. private function getPathForDestination(string $destinationPath): string {
  128. // get target node for proper path conversion
  129. if ($this->server->tree->nodeExists($destinationPath)) {
  130. $destinationNode = $this->server->tree->getNodeForPath($destinationPath);
  131. if (!$destinationNode instanceof Node) {
  132. throw new \Exception('Invalid destination node');
  133. }
  134. return $destinationNode->getPath();
  135. }
  136. $parent = dirname($destinationPath);
  137. if ($parent === '.') {
  138. $parent = '';
  139. }
  140. $parentNode = $this->server->tree->getNodeForPath($parent);
  141. if (!$parentNode instanceof Node) {
  142. throw new \Exception('Invalid destination node');
  143. }
  144. return $parentNode->getPath();
  145. }
  146. /**
  147. * This method is called before any HTTP method and validates there is enough free space to store the file
  148. *
  149. * @param string $path relative to the users home
  150. * @param int|float|null $length
  151. * @throws InsufficientStorage
  152. * @return bool
  153. */
  154. public function checkQuota(string $path, $length = null) {
  155. if ($length === null) {
  156. $length = $this->getLength();
  157. }
  158. if ($length) {
  159. [$parentPath, $newName] = \Sabre\Uri\split($path);
  160. if (is_null($parentPath)) {
  161. $parentPath = '';
  162. }
  163. $req = $this->server->httpRequest;
  164. // Strip any duplicate slashes
  165. $path = str_replace('//', '/', $path);
  166. $freeSpace = $this->getFreeSpace($path);
  167. if ($freeSpace >= 0 && $length > $freeSpace) {
  168. throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
  169. }
  170. }
  171. return true;
  172. }
  173. public function getLength() {
  174. $req = $this->server->httpRequest;
  175. $length = $req->getHeader('X-Expected-Entity-Length');
  176. if (!is_numeric($length)) {
  177. $length = $req->getHeader('Content-Length');
  178. $length = is_numeric($length) ? $length : null;
  179. }
  180. $ocLength = $req->getHeader('OC-Total-Length');
  181. if (!is_numeric($ocLength)) {
  182. return $length;
  183. }
  184. if (!is_numeric($length)) {
  185. return $ocLength;
  186. }
  187. return max($length, $ocLength);
  188. }
  189. /**
  190. * @param string $uri
  191. * @return mixed
  192. * @throws ServiceUnavailable
  193. */
  194. public function getFreeSpace($uri) {
  195. try {
  196. $freeSpace = $this->view->free_space(ltrim($uri, '/'));
  197. return $freeSpace;
  198. } catch (StorageNotAvailableException $e) {
  199. throw new ServiceUnavailable($e->getMessage());
  200. }
  201. }
  202. }