GoogleCloudStorage.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <?php
  2. namespace PrivateBin\Data;
  3. use Exception;
  4. use Google\Cloud\Core\Exception\NotFoundException;
  5. use Google\Cloud\Storage\Bucket;
  6. use Google\Cloud\Storage\StorageClient;
  7. use PrivateBin\Json;
  8. class GoogleCloudStorage extends AbstractData
  9. {
  10. /**
  11. * GCS client
  12. *
  13. * @access private
  14. * @var StorageClient
  15. */
  16. private $_client = null;
  17. /**
  18. * GCS bucket
  19. *
  20. * @access private
  21. * @var Bucket
  22. */
  23. private $_bucket = null;
  24. /**
  25. * object prefix
  26. *
  27. * @access private
  28. * @var string
  29. */
  30. private $_prefix = 'pastes';
  31. /**
  32. * bucket acl type
  33. *
  34. * @access private
  35. * @var bool
  36. */
  37. private $_uniformacl = false;
  38. /**
  39. * instantiantes a new Google Cloud Storage data backend.
  40. *
  41. * @access public
  42. * @param array $options
  43. */
  44. public function __construct(array $options)
  45. {
  46. if (getenv('PRIVATEBIN_GCS_BUCKET')) {
  47. $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
  48. }
  49. if (is_array($options) && array_key_exists('bucket', $options)) {
  50. $bucket = $options['bucket'];
  51. }
  52. if (is_array($options) && array_key_exists('prefix', $options)) {
  53. $this->_prefix = $options['prefix'];
  54. }
  55. if (is_array($options) && array_key_exists('uniformacl', $options)) {
  56. $this->_uniformacl = $options['uniformacl'];
  57. }
  58. $this->_client = class_exists('StorageClientStub', false) ?
  59. new \StorageClientStub(array()) :
  60. new StorageClient(array('suppressKeyFileNotice' => true));
  61. if (isset($bucket)) {
  62. $this->_bucket = $this->_client->bucket($bucket);
  63. }
  64. }
  65. /**
  66. * returns the google storage object key for $pasteid in $this->_bucket.
  67. *
  68. * @access private
  69. * @param $pasteid string to get the key for
  70. * @return string
  71. */
  72. private function _getKey($pasteid)
  73. {
  74. if ($this->_prefix != '') {
  75. return $this->_prefix . '/' . $pasteid;
  76. }
  77. return $pasteid;
  78. }
  79. /**
  80. * Uploads the payload in the $this->_bucket under the specified key.
  81. * The entire payload is stored as a JSON document. The metadata is replicated
  82. * as the GCS object's metadata except for the fields attachment, attachmentname
  83. * and salt.
  84. *
  85. * @param $key string to store the payload under
  86. * @param $payload array to store
  87. * @return bool true if successful, otherwise false.
  88. */
  89. private function _upload($key, $payload)
  90. {
  91. $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
  92. unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
  93. foreach ($metadata as $k => $v) {
  94. $metadata[$k] = strval($v);
  95. }
  96. try {
  97. $data = array(
  98. 'name' => $key,
  99. 'chunkSize' => 262144,
  100. 'metadata' => array(
  101. 'content-type' => 'application/json',
  102. 'metadata' => $metadata,
  103. ),
  104. );
  105. if (!$this->_uniformacl) {
  106. $data['predefinedAcl'] = 'private';
  107. }
  108. $this->_bucket->upload(Json::encode($payload), $data);
  109. } catch (Exception $e) {
  110. error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
  111. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  112. return false;
  113. }
  114. return true;
  115. }
  116. /**
  117. * @inheritDoc
  118. */
  119. public function create($pasteid, array $paste)
  120. {
  121. if ($this->exists($pasteid)) {
  122. return false;
  123. }
  124. return $this->_upload($this->_getKey($pasteid), $paste);
  125. }
  126. /**
  127. * @inheritDoc
  128. */
  129. public function read($pasteid)
  130. {
  131. try {
  132. $o = $this->_bucket->object($this->_getKey($pasteid));
  133. $data = $o->downloadAsString();
  134. return Json::decode($data);
  135. } catch (NotFoundException $e) {
  136. return false;
  137. } catch (Exception $e) {
  138. error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
  139. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  140. return false;
  141. }
  142. }
  143. /**
  144. * @inheritDoc
  145. */
  146. public function delete($pasteid)
  147. {
  148. $name = $this->_getKey($pasteid);
  149. try {
  150. foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
  151. try {
  152. $this->_bucket->object($comment->name())->delete();
  153. } catch (NotFoundException $e) {
  154. // ignore if already deleted.
  155. }
  156. }
  157. } catch (NotFoundException $e) {
  158. // there are no discussions associated with the paste
  159. }
  160. try {
  161. $this->_bucket->object($name)->delete();
  162. } catch (NotFoundException $e) {
  163. // ignore if already deleted
  164. }
  165. }
  166. /**
  167. * @inheritDoc
  168. */
  169. public function exists($pasteid)
  170. {
  171. $o = $this->_bucket->object($this->_getKey($pasteid));
  172. return $o->exists();
  173. }
  174. /**
  175. * @inheritDoc
  176. */
  177. public function createComment($pasteid, $parentid, $commentid, array $comment)
  178. {
  179. if ($this->existsComment($pasteid, $parentid, $commentid)) {
  180. return false;
  181. }
  182. $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
  183. return $this->_upload($key, $comment);
  184. }
  185. /**
  186. * @inheritDoc
  187. */
  188. public function readComments($pasteid)
  189. {
  190. $comments = array();
  191. $prefix = $this->_getKey($pasteid) . '/discussion/';
  192. try {
  193. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
  194. $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
  195. $comment['id'] = basename($key->name());
  196. $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
  197. $comments[$slot] = $comment;
  198. }
  199. } catch (NotFoundException $e) {
  200. // no comments found
  201. }
  202. return $comments;
  203. }
  204. /**
  205. * @inheritDoc
  206. */
  207. public function existsComment($pasteid, $parentid, $commentid)
  208. {
  209. $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
  210. $o = $this->_bucket->object($name);
  211. return $o->exists();
  212. }
  213. /**
  214. * @inheritDoc
  215. */
  216. public function purgeValues($namespace, $time)
  217. {
  218. $path = 'config/' . $namespace;
  219. try {
  220. foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
  221. $name = $object->name();
  222. if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
  223. continue;
  224. }
  225. $info = $object->info();
  226. if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
  227. $value = $info['metadata']['value'];
  228. if (is_numeric($value) && intval($value) < $time) {
  229. try {
  230. $object->delete();
  231. } catch (NotFoundException $e) {
  232. // deleted by another instance.
  233. }
  234. }
  235. }
  236. }
  237. } catch (NotFoundException $e) {
  238. // no objects in the bucket yet
  239. }
  240. }
  241. /**
  242. * For GoogleCloudStorage, the value will also be stored in the metadata for the
  243. * namespaces traffic_limiter and purge_limiter.
  244. * @inheritDoc
  245. */
  246. public function setValue($value, $namespace, $key = '')
  247. {
  248. if ($key === '') {
  249. $key = 'config/' . $namespace;
  250. } else {
  251. $key = 'config/' . $namespace . '/' . $key;
  252. }
  253. $metadata = array('namespace' => $namespace);
  254. if ($namespace != 'salt') {
  255. $metadata['value'] = strval($value);
  256. }
  257. try {
  258. $data = array(
  259. 'name' => $key,
  260. 'chunkSize' => 262144,
  261. 'metadata' => array(
  262. 'content-type' => 'application/json',
  263. 'metadata' => $metadata,
  264. ),
  265. );
  266. if (!$this->_uniformacl) {
  267. $data['predefinedAcl'] = 'private';
  268. }
  269. $this->_bucket->upload($value, $data);
  270. } catch (Exception $e) {
  271. error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
  272. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  273. return false;
  274. }
  275. return true;
  276. }
  277. /**
  278. * @inheritDoc
  279. */
  280. public function getValue($namespace, $key = '')
  281. {
  282. if ($key === '') {
  283. $key = 'config/' . $namespace;
  284. } else {
  285. $key = 'config/' . $namespace . '/' . $key;
  286. }
  287. try {
  288. $o = $this->_bucket->object($key);
  289. return $o->downloadAsString();
  290. } catch (NotFoundException $e) {
  291. return '';
  292. }
  293. }
  294. /**
  295. * @inheritDoc
  296. */
  297. protected function _getExpiredPastes($batchsize)
  298. {
  299. $expired = array();
  300. $now = time();
  301. $prefix = $this->_prefix;
  302. if ($prefix != '') {
  303. $prefix .= '/';
  304. }
  305. try {
  306. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
  307. $metadata = $object->info()['metadata'];
  308. if ($metadata != null && array_key_exists('expire_date', $metadata)) {
  309. $expire_at = intval($metadata['expire_date']);
  310. if ($expire_at != 0 && $expire_at < $now) {
  311. array_push($expired, basename($object->name()));
  312. }
  313. }
  314. if (count($expired) > $batchsize) {
  315. break;
  316. }
  317. }
  318. } catch (NotFoundException $e) {
  319. // no objects in the bucket yet
  320. }
  321. return $expired;
  322. }
  323. /**
  324. * @inheritDoc
  325. */
  326. public function getAllPastes()
  327. {
  328. $pastes = array();
  329. $prefix = $this->_prefix;
  330. if ($prefix != '') {
  331. $prefix .= '/';
  332. }
  333. try {
  334. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
  335. $candidate = substr($object->name(), strlen($prefix));
  336. if (strpos($candidate, '/') === false) {
  337. $pastes[] = $candidate;
  338. }
  339. }
  340. } catch (NotFoundException $e) {
  341. // no objects in the bucket yet
  342. }
  343. return $pastes;
  344. }
  345. }