GoogleCloudStorage.php 10 KB

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