From 7bfed5a48b7d8770188858e273c77a53efdc31d8 Mon Sep 17 00:00:00 2001 From: aymm Date: Tue, 24 Aug 2021 19:59:30 +0200 Subject: [PATCH] initial commit --- .gitignore | 45 ++++++ .htaccess | 2 + .nova/Configuration.json | 4 + composer.json | 8 + composer.lock | 165 +++++++++++++++++++ config.php.sample | 6 + index.php | 71 +++++++++ src/API/PhotoPrism.php | 177 +++++++++++++++++++++ src/Entities/Album.php | 18 +++ src/Exceptions/AuthenticationException.php | 7 + src/Exceptions/NetworkException.php | 7 + 11 files changed, 510 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 .nova/Configuration.json create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config.php.sample create mode 100644 index.php create mode 100644 src/API/PhotoPrism.php create mode 100644 src/Entities/Album.php create mode 100644 src/Exceptions/AuthenticationException.php create mode 100644 src/Exceptions/NetworkException.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed11775 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +**/*/logs +upload.php +config.php + +# Created by https://www.toptal.com/developers/gitignore/api/composer,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=composer,macos + +### Composer ### +composer.phar +/vendor/ + +# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.toptal.com/developers/gitignore/api/composer,macos \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..fc848cc --- /dev/null +++ b/.htaccess @@ -0,0 +1,2 @@ +php_value upload_max_filesize 100M +php_value post_max_size 100M \ No newline at end of file diff --git a/.nova/Configuration.json b/.nova/Configuration.json new file mode 100644 index 0000000..c8ae6dd --- /dev/null +++ b/.nova/Configuration.json @@ -0,0 +1,4 @@ +{ + "workspace.art_style" : 0, + "workspace.name" : "PhotoPrismUpload" +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7da28bf --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "monolog/monolog": "^2.3" + }, + "autoload": { + "psr-4": {"PhotoPrismUpload\\": "src"} + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..304b177 --- /dev/null +++ b/composer.lock @@ -0,0 +1,165 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a2eae41321c2d6bf3b57063bb8fe8c3b", + "packages": [ + { + "name": "monolog/monolog", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "71312564759a7db5b789296369c1a264efc43aad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/71312564759a7db5b789296369c1a264efc43aad", + "reference": "71312564759a7db5b789296369c1a264efc43aad", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7", + "graylog2/gelf-php": "^1.4.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpspec/prophecy": "^1.6.1", + "phpstan/phpstan": "^0.12.91", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", + "ruflin/elastica": ">=0.90 <7.0.1", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.3.2" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2021-07-23T07:42:52+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/config.php.sample b/config.php.sample new file mode 100644 index 0000000..0522db3 --- /dev/null +++ b/config.php.sample @@ -0,0 +1,6 @@ + '', + 'password' => '' +]; diff --git a/index.php b/index.php new file mode 100644 index 0000000..b2363e6 --- /dev/null +++ b/index.php @@ -0,0 +1,71 @@ + + + + + + + + +
+ + +
+ + + login(); + $api->getAlbums(); +} catch (\Exception $e) { + die('Fehler: ' . $e->getMessage()); +} diff --git a/src/API/PhotoPrism.php b/src/API/PhotoPrism.php new file mode 100644 index 0000000..20b465e --- /dev/null +++ b/src/API/PhotoPrism.php @@ -0,0 +1,177 @@ +api_url = $this->base_url.'/api/v1'; + $this->config = $config; + $this->logger = new Logger('PhotoPrismUpload'); + if (empty($log_path)) { + $log_path = __DIR__.'/logs/log.log'; + } + $handler = new RotatingFileHandler($log_path, 5, Logger::DEBUG, true); + $this->logger->pushHandler($handler); + if (isset($_SESSION['pp_sessionid'])) { + $this->session_id = $_SESSION['pp_sessionid']; + } + } + + private function parseHeaders(string $response): array + { + $response = explode("\r\n\r\n", $response, 2); + $headers = $response[0]; + if ($headers === 'HTTP/1.1 100 Continue') { + $response = explode("\r\n\r\n", $response[1], 2); + $headers = $response[0]; + } + if (isset($response[1])) { + $content = $response[1]; + } else { + $content = ''; + } + // this is not a good way to parse http headers + // it will not (for example) take into account multiline headers + // but what we're looking for is pretty basic, so we can ignore those shortcomings + $response_headers = explode("\r\n", $headers); + $header_arr = []; + foreach ($response_headers as $header) { + $header = explode(': ', $header, 2); + if (count($header) < 2) { + continue; + } + list($k,$v) = $header; + $header_arr[$k] = $v; + } + return ['headers' => $header_arr, 'content' => $content]; + } + + private function makeRequest( + string $method, + string $path, + array $data = [], + string $content_type = 'application/json' + ): string { + $url = $this->api_url.$path; + $method = strtolower($method); + $query_data = []; + $headers = []; + $result = null; + try { + if (is_array($data) && $method !== 'post-raw') { + $query_data = $content_type === 'application/json' ? json_encode($data) : http_build_query($data); + } + + $ch = curl_init(); + if ($method === 'get' && !empty($query_data)) { + $url .= '?'.$query_data; + } else { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query_data); + } + if ($method !== 'get') { + $headers[] = 'Content-Type: '.$content_type; + } + + if (!empty($this->session_id)) { + $headers[] = 'X-Session-Id: ' . $this->session_id; + } + $this->logger->info($method .' request to ' . $url); + $this->logger->debug('Headers ' . json_encode($headers)); + $this->logger->debug('postfields data ' . json_encode($data)); + $this->logger->debug('postfields ' . $query_data); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, $method !== 'get'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + $output = curl_exec($ch); + + $request = curl_getinfo($ch, CURLINFO_HEADER_OUT); + $http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($output === false) { + throw new NetworkException("Error sending request to " . $url, 0); + } + if (empty($output) || $output === false) { + $e = new NetworkException("No answer from" . $url, 0); + $this->logger->error("Error sending request", ['Exception' => $e]); + throw $e; + } + if ($http_status === 0) { + throw new NetworkException('Unable to connect to API ' . $url); + } + if ($http_status >= 400) { + throw new NetworkException('Invalid response ' . $url . ': ' . $http_status); + } + $result = $this->parseHeaders($output); + } catch (\Exception $e) { + $this->logger->error("Error sending request", ['Exception' => $e]); + throw new NetworkException("Error sending request to " . $url, 0, $e); + } + return $result['content']; + } + + public function login(bool $force = false) + { + if (!empty($this->session_id) && !$force) { + $this->logger->info('Skipping login, already logged in'); + return; + } + $data = [ + 'username' => $this->config['username'], + 'password' => $this->config['password'], + ]; + $res = $this->makeRequest('POST', '/session', $data); + $this->logger->info('Login result: ' . $res); + $response = json_decode($res, true); + if (!empty($response['error'])) { + throw new AuthenticationException($response['error']); + } + $this->session_id = $response['id']; + $_SESSION['pp_sessionid'] = $this->session_id; + $this->logger->debug('Session ID: ' . $this->session_id); + } + + public function getAlbums(int $count = 1000, int $offset = 0): array + { + $data = [ + 'count' => $count, + 'offset' => $offset, + 'type' => 'album' + ]; + $res = $this->makeRequest('GET', '/albums', $data, 'text/plain'); + $response = json_decode($res, true); + if (!empty($response['error'])) { + throw new NetworkException($response['error']); + } + $albums = array_map( + function ($entry) { + return new Album($entry); + }, + $response + ); + $this->logger->debug('Albums' . json_encode($albums)); + return $albums; + } + + #https://photos.phlaym.net/api/v1/albums?q=&count=1000&offset=0&type=album +} diff --git a/src/Entities/Album.php b/src/Entities/Album.php new file mode 100644 index 0000000..523780d --- /dev/null +++ b/src/Entities/Album.php @@ -0,0 +1,18 @@ +uid = $response['UID']; + $this->slug = $response['Slug']; + $this->title = $response['Title']; + } +} diff --git a/src/Exceptions/AuthenticationException.php b/src/Exceptions/AuthenticationException.php new file mode 100644 index 0000000..dbb275d --- /dev/null +++ b/src/Exceptions/AuthenticationException.php @@ -0,0 +1,7 @@ +