dbconnection->quote($str); } /** * @param string $userId * @param int $pruneBefore * @return array with devices */ public function getDevicesFromDB($userId) { $devices = []; $qb = $this->dbconnection->getQueryBuilder(); $qb->select('id', 'user_agent', 'color') ->from('maps_devices', 'd') ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); $req = $qb->executeQuery(); while ($row = $req->fetch()) { $devices[intval($row['id'])] = [ 'id' => intval($row['id']), 'user_agent' => $row['user_agent'], 'color' => $row['color'], 'isShareable' => true, 'isDeleteable' => true, 'isUpdateable' => true, 'isReadable' => true, 'shares' => [] ]; } $req->closeCursor(); return $devices; } /** * @param string[] $tokens * @return array * @throws Exception */ public function getDevicesByTokens(array $tokens) { $devices = []; $qb = $this->dbconnection->getquerybuilder(); $qb->select('d.id', 'd.user_agent', 'd.color', 's.token') ->from('maps_devices', 'd') ->innerJoin('d', 'maps_device_shares', 's', $qb->expr()->eq('d.id', 's.device_id')) ->where( $qb->expr()->in('s.token', $qb->createNamedParameter($tokens, IQueryBuilder::PARAM_STR_ARRAY)) ); $req = $qb->executeQuery(); while ($row = $req->fetch()) { if (array_key_exists(intval($row['id']), $devices)) { $devices[intval($row['id'])]['tokens'][] = $row['token']; } else { $devices[intval($row['id'])] = [ 'id' => intval($row['id']), 'user_agent' => $row['user_agent'], 'color' => $row['color'], 'isShareable' => false, 'isDeleteable' => true, 'isUpdateable' => false, 'isReadable' => true, 'shares' => [], 'tokens' => [$row['token']] ]; } } $req->closeCursor(); return $devices; } /** * @param $userId * @param $deviceId * @param int|null $pruneBefore * @param int|null $limit * @param int|null $offset * @return array * @throws \OCP\DB\Exception */ public function getDevicePointsFromDB($userId, $deviceId, ?int $pruneBefore = 0, ?int $limit = null, ?int $offset = null) { $qb = $this->dbconnection->getQueryBuilder(); // get coordinates $qb->selectDistinct(['p.id', 'lat', 'lng', 'timestamp', 'altitude', 'accuracy', 'battery']) ->from('maps_device_points', 'p') ->innerJoin('p', 'maps_devices', 'd', $qb->expr()->eq('d.id', 'p.device_id')) ->where( $qb->expr()->eq('p.device_id', $qb->createNamedParameter($deviceId, IQueryBuilder::PARAM_INT)) ) ->andWhere( $qb->expr()->eq('d.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); if (intval($pruneBefore) > 0) { $qb->andWhere( $qb->expr()->gt('timestamp', $qb->createNamedParameter(intval($pruneBefore), IQueryBuilder::PARAM_INT)) ); } if (!is_null($offset)) { $qb->setFirstResult($offset); } if (!is_null($limit)) { $qb->setMaxResults($limit); } $qb->orderBy('timestamp', 'DESC'); $req = $qb->executeQuery(); $points = []; while ($row = $req->fetch()) { $points[] = [ 'id' => intval($row['id']), 'lat' => floatval($row['lat']), 'lng' => floatval($row['lng']), 'timestamp' => intval($row['timestamp']), 'altitude' => is_numeric($row['altitude']) ? floatval($row['altitude']) : null, 'accuracy' => is_numeric($row['accuracy']) ? floatval($row['accuracy']) : null, 'battery' => is_numeric($row['battery']) ? floatval($row['battery']) : null ]; } $req->closeCursor(); return array_reverse($points); } /** * @param string[] $token * @param int|null $pruneBefore * @param int|null $limit * @param int|null $offset * @return array * @throws Exception */ public function getDevicePointsByTokens(array $tokens, ?int $pruneBefore = 0, ?int $limit = 10000, ?int $offset = 0) { $qb = $this->dbconnection->getQueryBuilder(); // get coordinates $or = []; foreach ($tokens as $token) { $or[] = $qb->expr()->andX( $qb->expr()->eq('s.token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)), $qb->expr()->lte('p.timestamp', 's.timestamp_to'), $qb->expr()->gte('p.timestamp', 's.timestamp_from') ); } $qb->select('p.id', 'lat', 'lng', 'timestamp', 'altitude', 'accuracy', 'battery') ->from('maps_device_points', 'p') ->innerJoin('p', 'maps_device_shares', 's', $qb->expr()->eq('p.device_id', 's.device_id')) ->where($qb->expr()->orX(...$or)); if (intval($pruneBefore) > 0) { $qb->andWhere( $qb->expr()->gt('timestamp', $qb->createNamedParameter(intval($pruneBefore), IQueryBuilder::PARAM_INT)) ); } if (!is_null($offset)) { $qb->setFirstResult($offset); } if (!is_null($limit)) { $qb->setMaxResults($limit); } $qb->orderBy('timestamp', 'DESC'); $req = $qb->executeQuery(); $points = []; while ($row = $req->fetch()) { $points[] = [ 'id' => intval($row['id']), 'lat' => floatval($row['lat']), 'lng' => floatval($row['lng']), 'timestamp' => intval($row['timestamp']), 'altitude' => is_numeric($row['altitude']) ? floatval($row['altitude']) : null, 'accuracy' => is_numeric($row['accuracy']) ? floatval($row['accuracy']) : null, 'battery' => is_numeric($row['battery']) ? floatval($row['battery']) : null ]; } $req->closeCursor(); return array_reverse($points); } /** * @param $userId * @param $deviceId * @return array * @throws Exception */ public function getDeviceTimePointsFromDb($userId, $deviceId) { $qb = $this->dbconnection->getQueryBuilder(); // get coordinates $qb->select('lat', 'lng', 'timestamp') ->from('maps_device_points', 'p') ->innerJoin('p', 'maps_devices', 'd', $qb->expr()->eq('d.id', 'p.device_id')) ->where( $qb->expr()->eq('p.device_id', $qb->createNamedParameter($deviceId, IQueryBuilder::PARAM_INT)) ) ->andWhere( $qb->expr()->eq('d.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); $qb->orderBy('timestamp', 'ASC'); $req = $qb->executeQuery(); $points = []; while ($row = $req->fetch()) { $points[intval($row['timestamp'])] = [floatval($row['lat']), floatval($row['lng'])]; } $req->closeCursor(); return $points; } public function getOrCreateDeviceFromDB($userId, $userAgent) { $deviceId = null; $qb = $this->dbconnection->getQueryBuilder(); $qb->select('id') ->from('maps_devices', 'd') ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ) ->andWhere( $qb->expr()->eq('user_agent', $qb->createNamedParameter($userAgent, IQueryBuilder::PARAM_STR)) ); $req = $qb->executeQuery(); while ($row = $req->fetch()) { $deviceId = intval($row['id']); break; } $req->closeCursor(); if ($deviceId === null) { $qb->insert('maps_devices') ->values([ 'user_agent' => $qb->createNamedParameter($userAgent, IQueryBuilder::PARAM_STR), 'user_id' => $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR) ]); $qb->executeStatement(); $deviceId = $qb->getLastInsertId(); } return $deviceId; } public function addPointToDB($deviceId, $lat, $lng, $ts, $altitude, $battery, $accuracy) { $qb = $this->dbconnection->getQueryBuilder(); $qb->insert('maps_device_points') ->values([ 'device_id' => $qb->createNamedParameter($deviceId, IQueryBuilder::PARAM_STR), 'lat' => $qb->createNamedParameter($lat, IQueryBuilder::PARAM_STR), 'lng' => $qb->createNamedParameter($lng, IQueryBuilder::PARAM_STR), 'timestamp' => $qb->createNamedParameter(intval($ts), IQueryBuilder::PARAM_INT), 'altitude' => $qb->createNamedParameter(is_numeric($altitude) ? $altitude : null, IQueryBuilder::PARAM_STR), 'battery' => $qb->createNamedParameter(is_numeric($battery) ? $battery : null, IQueryBuilder::PARAM_STR), 'accuracy' => $qb->createNamedParameter(is_numeric($accuracy) ? $accuracy : null, IQueryBuilder::PARAM_STR) ]); $qb->executeStatement(); $pointId = $qb->getLastInsertId(); return $pointId; } public function addPointsToDB($deviceId, $points) { $values = []; foreach ($points as $p) { $value = '(' . $this->db_quote_escape_string($deviceId) . ', ' . $this->db_quote_escape_string($p['lat']) . ', ' . $this->db_quote_escape_string($p['lng']) . ', ' . $this->db_quote_escape_string($p['date']) . ', ' . ((isset($p['altitude']) and is_numeric($p['altitude'])) ? $this->db_quote_escape_string(floatval($p['altitude'])) : 'NULL') . ', ' . ((isset($p['battery']) and is_numeric($p['battery'])) ? $this->db_quote_escape_string(floatval($p['battery'])) : 'NULL') . ', ' . ((isset($p['accuracy']) and is_numeric($p['accuracy'])) ? $this->db_quote_escape_string(floatval($p['accuracy'])) : 'NULL') . ')'; array_push($values, $value); } $valuesStr = implode(', ', $values); $sql = ' INSERT INTO *PREFIX*maps_device_points (device_id, lat, lng, timestamp, altitude, battery, accuracy) VALUES ' . $valuesStr . ' ;'; $req = $this->dbconnection->prepare($sql); $req->execute(); $req->closeCursor(); } public function getDeviceFromDB($id, $userId) { $device = null; $qb = $this->dbconnection->getQueryBuilder(); $qb->select('id', 'user_agent', 'color') ->from('maps_devices', 'd') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); if ($userId !== null) { $qb->andWhere( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); } $req = $qb->executeQuery(); while ($row = $req->fetch()) { $device = [ 'id' => intval($row['id']), 'user_agent' => $row['user_agent'], 'color' => $row['color'] ]; break; } $req->closeCursor(); return $device; } public function editDeviceInDB($id, $color, $name) { $qb = $this->dbconnection->getQueryBuilder(); $qb->update('maps_devices'); if (is_string($color) && strlen($color) > 0) { $qb->set('color', $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR)); } if (is_string($name) && strlen($name) > 0) { $qb->set('user_agent', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); } $qb->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); $qb->executeStatement(); } public function deleteDeviceFromDB($id) { $qb = $this->dbconnection->getQueryBuilder(); $qb->delete('maps_devices') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); $qb->executeStatement(); $qb->delete('maps_device_points') ->where( $qb->expr()->eq('device_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); $qb->executeStatement(); } public function countPoints($userId, $deviceIdList, $begin, $end) { $qb = $this->dbconnection->getQueryBuilder(); $qb->select($qb->createFunction('COUNT(*) AS co')) ->from('maps_devices', 'd') ->innerJoin('d', 'maps_device_points', 'p', $qb->expr()->eq('d.id', 'p.device_id')) ->where( $qb->expr()->eq('d.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); if (is_array($deviceIdList) and count($deviceIdList) > 0) { $or = $qb->expr()->orx(); foreach ($deviceIdList as $deviceId) { $or->add($qb->expr()->eq('d.id', $qb->createNamedParameter($deviceId, IQueryBuilder::PARAM_INT))); } $qb->andWhere($or); } else { return 0; } if ($begin !== null && is_numeric($begin)) { $qb->andWhere( $qb->expr()->gt('p.timestamp', $qb->createNamedParameter(intval($begin), IQueryBuilder::PARAM_INT)) ); } if ($end !== null && is_numeric($end)) { $qb->andWhere( $qb->expr()->lt('p.timestamp', $qb->createNamedParameter(intval($end), IQueryBuilder::PARAM_INT)) ); } $req = $qb->executeQuery(); $count = 0; while ($row = $req->fetch()) { $count = intval($row['co']); break; } return $count; } public function exportDevices($userId, $handler, $deviceIdList, $begin, $end, $appVersion, $filename) { $gpxHeader = $this->generateGpxHeader($filename, $appVersion, count($deviceIdList)); fwrite($handler, $gpxHeader); foreach ($deviceIdList as $devid) { $nbPoints = $this->countPoints($userId, [$devid], $begin, $end); if ($nbPoints > 0) { $this->getAndWriteDevicePoints($devid, $begin, $end, $handler, $nbPoints, $userId); } } fwrite($handler, ''); } private function generateGpxHeader($name, $appVersion, $nbdev = 0) { date_default_timezone_set('UTC'); $dt = new \DateTime(); $date = $dt->format('Y-m-d\TH:i:s\Z'); $gpxText = '' . "\n"; $gpxText .= '' . "\n"; $gpxText .= '' . "\n" . ' ' . "\n"; $gpxText .= ' ' . $name . '' . "\n"; if ($nbdev > 0) { $gpxText .= ' ' . $nbdev . ' device' . ($nbdev > 1 ? 's' : '') . '' . "\n"; } $gpxText .= '' . "\n"; return $gpxText; } private function getAndWriteDevicePoints($devid, $begin, $end, $fd, $nbPoints, $userId) { $device = $this->getDeviceFromDB($devid, $userId); $devname = $device['user_agent']; $qb = $this->dbconnection->getQueryBuilder(); $gpxText = '' . "\n" . ' ' . $devname . '' . "\n"; $gpxText .= ' ' . "\n"; fwrite($fd, $gpxText); $chunkSize = 10000; $pointIndex = 0; while ($pointIndex < $nbPoints) { $gpxText = ''; $qb->select('id', 'lat', 'lng', 'timestamp', 'altitude', 'accuracy', 'battery') ->from('maps_device_points', 'p') ->where( $qb->expr()->eq('device_id', $qb->createNamedParameter($devid, IQueryBuilder::PARAM_INT)) ); if (intval($begin) > 0) { $qb->andWhere( $qb->expr()->gt('timestamp', $qb->createNamedParameter(intval($begin), IQueryBuilder::PARAM_INT)) ); } if (intval($end) > 0) { $qb->andWhere( $qb->expr()->lt('timestamp', $qb->createNamedParameter(intval($end), IQueryBuilder::PARAM_INT)) ); } $qb->setFirstResult($pointIndex); $qb->setMaxResults($chunkSize); $qb->orderBy('timestamp', 'ASC'); $req = $qb->executeQuery(); while ($row = $req->fetch()) { $id = intval($row['id']); $lat = floatval($row['lat']); $lng = floatval($row['lng']); $epoch = $row['timestamp']; $date = ''; if (is_numeric($epoch)) { $epoch = intval($epoch); $dt = new \DateTime("@$epoch"); $date = $dt->format('Y-m-d\TH:i:s\Z'); } $alt = $row['altitude']; $acc = $row['accuracy']; $bat = $row['battery']; $gpxExtension = ''; $gpxText .= ' ' . "\n"; $gpxText .= ' ' . "\n"; if (is_numeric($alt)) { $gpxText .= ' ' . sprintf('%.2f', floatval($alt)) . '' . "\n"; } if (is_numeric($acc) && intval($acc) >= 0) { $gpxExtension .= ' ' . sprintf('%.2f', floatval($acc)) . '' . "\n"; } if (is_numeric($bat) && intval($bat) >= 0) { $gpxExtension .= ' ' . sprintf('%.2f', floatval($bat)) . '' . "\n"; } if ($gpxExtension !== '') { $gpxText .= ' ' . "\n" . $gpxExtension; $gpxText .= ' ' . "\n"; } $gpxText .= ' ' . "\n"; } $req->closeCursor(); // write the chunk fwrite($fd, $gpxText); $pointIndex = $pointIndex + $chunkSize; } $gpxText = ' ' . "\n"; $gpxText .= '' . "\n"; fwrite($fd, $gpxText); } public function importDevices($userId, $file) { $lowerFileName = strtolower($file->getName()); if ($this->endswith($lowerFileName, '.gpx')) { return $this->importDevicesFromGpx($userId, $file); } elseif ($this->endswith($lowerFileName, '.kml')) { $fp = $file->fopen('r'); $name = $file->getName(); return $this->importDevicesFromKml($userId, $fp, $name); } elseif ($this->endswith($lowerFileName, '.kmz')) { return $this->importDevicesFromKmz($userId, $file); } } public function importDevicesFromGpx($userId, $file) { $this->currentPointList = []; $this->importUserId = $userId; $this->importFileName = $file->getName(); $this->trackIndex = 1; $this->insideTrk = false; $xml_parser = xml_parser_create(); xml_set_object($xml_parser, $this); xml_set_element_handler($xml_parser, 'gpxStartElement', 'gpxEndElement'); xml_set_character_data_handler($xml_parser, 'gpxDataElement'); $fp = $file->fopen('r'); // using xml_parse to be able to parse file chunks in case it's too big while ($data = fread($fp, 4096000)) { if (!xml_parse($xml_parser, $data, feof($fp))) { $this->logger->error( 'Exception in ' . $file->getName() . ' parsing at line ' . xml_get_current_line_number($xml_parser) . ' : ' . xml_error_string(xml_get_error_code($xml_parser)), ['app' => 'maps'] ); return 0; } } fclose($fp); xml_parser_free($xml_parser); return ($this->trackIndex - 1); } private function gpxStartElement($parser, $name, $attrs) { //$points, array($lat, $lon, $ele, $timestamp, $acc, $bat, $sat, $ua, $speed, $bearing) $this->currentXmlTag = $name; if ($name === 'TRK') { $this->importDevName = ''; $this->pointIndex = 1; $this->currentPointList = []; $this->insideTrk = true; } elseif ($name === 'TRKPT') { $this->currentPoint = []; if (isset($attrs['LAT'])) { $this->currentPoint['lat'] = floatval($attrs['LAT']); } if (isset($attrs['LON'])) { $this->currentPoint['lng'] = floatval($attrs['LON']); } } //var_dump($attrs); } private function gpxEndElement($parser, $name) { if ($name === 'TRK') { $this->insideTrk = false; // log last track points if (count($this->currentPointList) > 0) { if ($this->importDevName === '') { $this->importDevName = $this->importFileName . ' ' . $this->trackIndex; } $devid = $this->getOrCreateDeviceFromDB($this->importUserId, $this->importDevName); $this->addPointsToDB($devid, $this->currentPointList); } $this->trackIndex++; unset($this->currentPointList); } elseif ($name === 'TRKPT') { // store track point // convert date if (isset($this->currentPoint['date'])) { $time = new \DateTime($this->currentPoint['date']); $timestamp = $time->getTimestamp(); $this->currentPoint['date'] = $timestamp; } array_push($this->currentPointList, $this->currentPoint); // if we have enough points, we log them and clean the points array if (count($this->currentPointList) >= 500) { if ($this->importDevName === '') { $this->importDevName = 'device' . $this->trackIndex; } $devid = $this->getOrCreateDeviceFromDB($this->importUserId, $this->importDevName); $this->addPointsToDB($devid, $this->currentPointList); unset($this->currentPointList); $this->currentPointList = []; } $this->pointIndex++; } } private function gpxDataElement($parser, $data) { $d = trim($data); if (!empty($d)) { if ($this->currentXmlTag === 'ELE') { $this->currentPoint['altitude'] = (isset($this->currentPoint['altitude'])) ? $this->currentPoint['altitude'] . $d : $d; } elseif ($this->currentXmlTag === 'BATTERYLEVEL') { $this->currentPoint['battery'] = (isset($this->currentPoint['battery'])) ? $this->currentPoint['battery'] . $d : $d; } elseif ($this->currentXmlTag === 'ACCURACY') { $this->currentPoint['accuracy'] = (isset($this->currentPoint['accuracy'])) ? $this->currentPoint['accuracy'] . $d : $d; } elseif ($this->insideTrk and $this->currentXmlTag === 'TIME') { $this->currentPoint['date'] = (isset($this->currentPoint['date'])) ? $this->currentPoint['date'] . $d : $d; } elseif ($this->insideTrk and $this->currentXmlTag === 'NAME') { $this->importDevName = $this->importDevName . $d; } } } public function importDevicesFromKmz($userId, $file) { $path = $file->getStorage()->getLocalFile($file->getInternalPath()); $name = $file->getName(); $zf = new ZIP($path); if (count($zf->getFiles()) > 0) { $zippedFilePath = $zf->getFiles()[0]; $fstream = $zf->getStream($zippedFilePath, 'r'); $nbImported = $this->importDevicesFromKml($userId, $fstream, $name); } else { $nbImported = 0; } return $nbImported; } public function importDevicesFromKml($userId, $fp, $name) { $this->trackIndex = 1; $this->importUserId = $userId; $this->importFileName = $name; $xml_parser = xml_parser_create(); xml_set_object($xml_parser, $this); xml_set_element_handler($xml_parser, 'kmlStartElement', 'kmlEndElement'); xml_set_character_data_handler($xml_parser, 'kmlDataElement'); while ($data = fread($fp, 4096000)) { if (!xml_parse($xml_parser, $data, feof($fp))) { $this->logger->error( 'Exception in ' . $name . ' parsing at line ' . xml_get_current_line_number($xml_parser) . ' : ' . xml_error_string(xml_get_error_code($xml_parser)), ); return 0; } } fclose($fp); xml_parser_free($xml_parser); return ($this->trackIndex - 1); } private function kmlStartElement($parser, $name, $attrs) { $this->currentXmlTag = $name; if ($name === 'GX:TRACK') { if (isset($attrs['ID'])) { $this->importDevName = $attrs['ID']; } else { $this->importDevName = $this->importFileName . ' ' . $this->trackIndex; } $this->pointIndex = 1; $this->currentPointList = []; } elseif ($name === 'WHEN') { $this->currentPoint = []; } //var_dump($attrs); } private function kmlEndElement($parser, $name) { if ($name === 'GX:TRACK') { // log last track points if (count($this->currentPointList) > 0) { $devid = $this->getOrCreateDeviceFromDB($this->importUserId, $this->importDevName); $this->addPointsToDB($devid, $this->currentPointList); } $this->trackIndex++; unset($this->currentPointList); } elseif ($name === 'GX:COORD') { // convert date if (isset($this->currentPoint['date'])) { $time = new \DateTime($this->currentPoint['date']); $timestamp = $time->getTimestamp(); $this->currentPoint['date'] = $timestamp; } // get latlng if (isset($this->currentPoint['coords'])) { $spl = explode(' ', $this->currentPoint['coords']); if (count($spl) > 1) { $this->currentPoint['lat'] = floatval($spl[1]); $this->currentPoint['lng'] = floatval($spl[0]); if (count($spl) > 2) { $this->currentPoint['altitude'] = floatval($spl[2]); } } } // store track point array_push($this->currentPointList, $this->currentPoint); // if we have enough points, we log them and clean the points array if (count($this->currentPointList) >= 500) { $devid = $this->getOrCreateDeviceFromDB($this->importUserId, $this->importDevName); $this->addPointsToDB($devid, $this->currentPointList); unset($this->currentPointList); $this->currentPointList = []; } $this->pointIndex++; } } private function kmlDataElement($parser, $data) { $d = trim($data); if (!empty($d)) { if ($this->currentXmlTag === 'WHEN') { $this->currentPoint['date'] = (isset($this->currentPoint['date'])) ? $this->currentPoint['date'] . $d : $d; } elseif ($this->currentXmlTag === 'GX:COORD') { $this->currentPoint['coords'] = (isset($this->currentPoint['coords'])) ? $this->currentPoint['coords'] . $d : $d; } } } private function endswith($string, $test) { $strlen = strlen($string); $testlen = strlen($test); if ($testlen > $strlen) { return false; } return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0; } /** * @param $folder * @param bool $isCreatable * @return mixed * @throws NotFoundException */ public function getSharedDevicesFromFolder($folder, bool $isCreatable = true) { try { $file = $folder->get('.device_shares.json'); } catch (NotFoundException $e) { if ($isCreatable) { $file = $folder->newFile('.device_shares.json', $content = '[]'); } else { throw new NotFoundException(); } } return json_decode($file->getContent(), true); } }