GoogleCloudStorage.php 10 KB

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