GoogleCloudStorage.php 10 KB

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