GoogleCloudStorage.php 11 KB

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