GoogleCloudStorage.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
  104. unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
  105. foreach ($metadata as $k => $v) {
  106. $metadata[$k] = strval($v);
  107. }
  108. try {
  109. $data = array(
  110. 'name' => $key,
  111. 'chunkSize' => 262144,
  112. 'metadata' => array(
  113. 'content-type' => 'application/json',
  114. 'metadata' => $metadata,
  115. ),
  116. );
  117. if (!self::$_uniformacl) {
  118. $data['predefinedAcl'] = 'private';
  119. }
  120. self::$_bucket->upload(Json::encode($payload), $data);
  121. } catch (Exception $e) {
  122. error_log('failed to upload ' . $key . ' to ' . self::$_bucket->name() . ', ' .
  123. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  124. return false;
  125. }
  126. return true;
  127. }
  128. /**
  129. * @inheritDoc
  130. */
  131. public function create($pasteid, array $paste)
  132. {
  133. if ($this->exists($pasteid)) {
  134. return false;
  135. }
  136. return $this->_upload($this->_getKey($pasteid), $paste);
  137. }
  138. /**
  139. * @inheritDoc
  140. */
  141. public function read($pasteid)
  142. {
  143. try {
  144. $o = self::$_bucket->object($this->_getKey($pasteid));
  145. $data = $o->downloadAsString();
  146. return Json::decode($data);
  147. } catch (NotFoundException $e) {
  148. return false;
  149. } catch (Exception $e) {
  150. error_log('failed to read ' . $pasteid . ' from ' . self::$_bucket->name() . ', ' .
  151. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  152. return false;
  153. }
  154. }
  155. /**
  156. * @inheritDoc
  157. */
  158. public function delete($pasteid)
  159. {
  160. $name = $this->_getKey($pasteid);
  161. try {
  162. foreach (self::$_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
  163. try {
  164. self::$_bucket->object($comment->name())->delete();
  165. } catch (NotFoundException $e) {
  166. // ignore if already deleted.
  167. }
  168. }
  169. } catch (NotFoundException $e) {
  170. // there are no discussions associated with the paste
  171. }
  172. try {
  173. self::$_bucket->object($name)->delete();
  174. } catch (NotFoundException $e) {
  175. // ignore if already deleted
  176. }
  177. }
  178. /**
  179. * @inheritDoc
  180. */
  181. public function exists($pasteid)
  182. {
  183. $o = self::$_bucket->object($this->_getKey($pasteid));
  184. return $o->exists();
  185. }
  186. /**
  187. * @inheritDoc
  188. */
  189. public function createComment($pasteid, $parentid, $commentid, array $comment)
  190. {
  191. if ($this->existsComment($pasteid, $parentid, $commentid)) {
  192. return false;
  193. }
  194. $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
  195. return $this->_upload($key, $comment);
  196. }
  197. /**
  198. * @inheritDoc
  199. */
  200. public function readComments($pasteid)
  201. {
  202. $comments = array();
  203. $prefix = $this->_getKey($pasteid) . '/discussion/';
  204. try {
  205. foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $key) {
  206. $comment = JSON::decode(self::$_bucket->object($key->name())->downloadAsString());
  207. $comment['id'] = basename($key->name());
  208. $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
  209. $comments[$slot] = $comment;
  210. }
  211. } catch (NotFoundException $e) {
  212. // no comments found
  213. }
  214. return $comments;
  215. }
  216. /**
  217. * @inheritDoc
  218. */
  219. public function existsComment($pasteid, $parentid, $commentid)
  220. {
  221. $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
  222. $o = self::$_bucket->object($name);
  223. return $o->exists();
  224. }
  225. /**
  226. * @inheritDoc
  227. */
  228. public function purgeValues($namespace, $time)
  229. {
  230. $path = 'config/' . $namespace;
  231. try {
  232. foreach (self::$_bucket->objects(array('prefix' => $path)) as $object) {
  233. $name = $object->name();
  234. if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
  235. continue;
  236. }
  237. $info = $object->info();
  238. if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
  239. $value = $info['metadata']['value'];
  240. if (is_numeric($value) && intval($value) < $time) {
  241. try {
  242. $object->delete();
  243. } catch (NotFoundException $e) {
  244. // deleted by another instance.
  245. }
  246. }
  247. }
  248. }
  249. } catch (NotFoundException $e) {
  250. // no objects in the bucket yet
  251. }
  252. }
  253. /**
  254. * For GoogleCloudStorage, the value will also be stored in the metadata for the
  255. * namespaces traffic_limiter and purge_limiter.
  256. * @inheritDoc
  257. */
  258. public function setValue($value, $namespace, $key = '')
  259. {
  260. if ($key === '') {
  261. $key = 'config/' . $namespace;
  262. } else {
  263. $key = 'config/' . $namespace . '/' . $key;
  264. }
  265. $metadata = array('namespace' => $namespace);
  266. if ($namespace != 'salt') {
  267. $metadata['value'] = strval($value);
  268. }
  269. try {
  270. $data = array(
  271. 'name' => $key,
  272. 'chunkSize' => 262144,
  273. 'metadata' => array(
  274. 'content-type' => 'application/json',
  275. 'metadata' => $metadata,
  276. ),
  277. );
  278. if (!self::$_uniformacl) {
  279. $data['predefinedAcl'] = 'private';
  280. }
  281. self::$_bucket->upload($value, $data);
  282. } catch (Exception $e) {
  283. error_log('failed to set key ' . $key . ' to ' . self::$_bucket->name() . ', ' .
  284. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  285. return false;
  286. }
  287. return true;
  288. }
  289. /**
  290. * @inheritDoc
  291. */
  292. public function getValue($namespace, $key = '')
  293. {
  294. if ($key === '') {
  295. $key = 'config/' . $namespace;
  296. } else {
  297. $key = 'config/' . $namespace . '/' . $key;
  298. }
  299. try {
  300. $o = self::$_bucket->object($key);
  301. return $o->downloadAsString();
  302. } catch (NotFoundException $e) {
  303. return '';
  304. }
  305. }
  306. /**
  307. * @inheritDoc
  308. */
  309. protected function _getExpiredPastes($batchsize)
  310. {
  311. $expired = array();
  312. $now = time();
  313. $prefix = self::$_prefix;
  314. if ($prefix != '') {
  315. $prefix .= '/';
  316. }
  317. try {
  318. foreach (self::$_bucket->objects(array('prefix' => $prefix)) as $object) {
  319. $metadata = $object->info()['metadata'];
  320. if ($metadata != null && array_key_exists('expire_date', $metadata)) {
  321. $expire_at = intval($metadata['expire_date']);
  322. if ($expire_at != 0 && $expire_at < $now) {
  323. array_push($expired, basename($object->name()));
  324. }
  325. }
  326. if (count($expired) > $batchsize) {
  327. break;
  328. }
  329. }
  330. } catch (NotFoundException $e) {
  331. // no objects in the bucket yet
  332. }
  333. return $expired;
  334. }
  335. }