Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c16245040 | |||
| c6df7f956d | |||
| a43d38d87f | |||
| c6e4333395 |
17
README.md
17
README.md
@@ -2,11 +2,28 @@
|
|||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](LICENSE.txt)
|
[](LICENSE.txt)
|
||||||
|
[](https://packagist.org/packages/din9xtr/cloud-control-panel)
|
||||||
|
|
||||||
A lightweight, self-hosted cloud management panel designed for simplicity and performance. Built with modern PHP and
|
A lightweight, self-hosted cloud management panel designed for simplicity and performance. Built with modern PHP and
|
||||||
containerized for easy deployment, it provides an intuitive interface for managing your personal cloud storage with
|
containerized for easy deployment, it provides an intuitive interface for managing your personal cloud storage with
|
||||||
minimal resource overhead.
|
minimal resource overhead.
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### Via Composer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer create-project din9xtr/cloud-control-panel my-cloud
|
||||||
|
cd my-cloud
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/din9xtr/cloud_control_panel.git
|
||||||
|
cd cloud_control_panel
|
||||||
|
```
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
1. Minimal footprint - Low memory and CPU usage
|
1. Minimal footprint - Low memory and CPU usage
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
"relay/relay": "^3.0",
|
"relay/relay": "^3.0",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-fileinfo": "*",
|
"ext-fileinfo": "*",
|
||||||
"ext-zip": "*"
|
"ext-zip": "*",
|
||||||
|
"php-http/curl-client": "^2.4",
|
||||||
|
"ext-sodium": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^2.1",
|
"phpstan/phpstan": "^2.1",
|
||||||
@@ -59,7 +61,8 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"phpstan/extension-installer": true
|
"phpstan/extension-installer": true,
|
||||||
|
"php-http/discovery": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
db/migration/007_create_icloud_accounts.php
Normal file
26
db/migration/007_create_icloud_accounts.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return function (PDO $db): void {
|
||||||
|
$db->exec("PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS icloud_accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
remote_name TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
apple_id TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
|
||||||
|
trust_token TEXT NOT NULL,
|
||||||
|
cookies TEXT NOT NULL,
|
||||||
|
|
||||||
|
status TEXT NOT NULL DEFAULT 'connected',
|
||||||
|
connected_at INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
");
|
||||||
|
};
|
||||||
@@ -12,16 +12,40 @@ services:
|
|||||||
- ./db:/var/db
|
- ./db:/var/db
|
||||||
- ./php/php.ini:/usr/local/etc/php/conf.d/uploads.ini
|
- ./php/php.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||||
- ./Caddyfile:/etc/frankenphp/Caddyfile
|
- ./Caddyfile:/etc/frankenphp/Caddyfile
|
||||||
# - caddy_data:/data
|
# - caddy_data:/data
|
||||||
# - caddy_config:/config
|
# - caddy_config:/config
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
tty: false
|
tty: false
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "curl", "-kf", "http://localhost/" ]
|
test: [ "CMD", "curl", "-kf", "http://localhost/" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
rclone:
|
||||||
|
image: rclone/rclone:latest
|
||||||
|
container_name: rclone
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- rcd
|
||||||
|
- --rc-addr=:5572
|
||||||
|
- --rc-user=${RCLONE_USER}
|
||||||
|
- --rc-pass=${RCLONE_PASS}
|
||||||
|
- --config=/config/rclone.conf
|
||||||
|
- --cache-dir=/cache
|
||||||
|
volumes:
|
||||||
|
- rclone_config:/config
|
||||||
|
- rclone_cache:/cache
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
rclone_config:
|
||||||
|
rclone_cache:
|
||||||
#volumes:
|
#volumes:
|
||||||
# caddy_data:
|
# caddy_data:
|
||||||
# caddy_config:
|
# caddy_config:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ opcache.enable_cli = 1
|
|||||||
opcache.memory_consumption = 256
|
opcache.memory_consumption = 256
|
||||||
opcache.max_accelerated_files = 20000
|
opcache.max_accelerated_files = 20000
|
||||||
opcache.interned_strings_buffer = 16
|
opcache.interned_strings_buffer = 16
|
||||||
opcache.validate_timestamps = 0
|
opcache.validate_timestamps = 1
|
||||||
opcache.revalidate_freq = 2
|
opcache.revalidate_freq = 2
|
||||||
opcache.max_wasted_percentage = 50
|
opcache.max_wasted_percentage = 50
|
||||||
; JIT
|
; JIT
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Din9xtrCloud\Controllers\ICloudAuthController;
|
||||||
|
use Din9xtrCloud\Rclone\RcloneClient;
|
||||||
|
use Http\Client\Curl\Client;
|
||||||
use Din9xtrCloud\App;
|
use Din9xtrCloud\App;
|
||||||
use Din9xtrCloud\Container\Container;
|
use Din9xtrCloud\Container\Container;
|
||||||
use Din9xtrCloud\Controllers\AuthController;
|
use Din9xtrCloud\Controllers\AuthController;
|
||||||
@@ -23,7 +26,11 @@ use Monolog\Processor\PsrLogMessageProcessor;
|
|||||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use Nyholm\Psr7Server\ServerRequestCreator;
|
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
use Psr\Http\Message\RequestFactoryInterface;
|
||||||
|
use Psr\Http\Message\ResponseFactoryInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\StreamFactoryInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -77,6 +84,30 @@ $container->request(
|
|||||||
)->fromGlobals()
|
)->fromGlobals()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$container->singleton(ClientInterface::class, fn() => new Client());
|
||||||
|
|
||||||
|
$container->singleton(
|
||||||
|
ResponseFactoryInterface::class,
|
||||||
|
fn(Container $c) => $c->get(Psr17Factory::class)
|
||||||
|
);
|
||||||
|
$container->singleton(RequestFactoryInterface::class,
|
||||||
|
fn(Container $c) => $c->get(Psr17Factory::class)
|
||||||
|
);
|
||||||
|
|
||||||
|
$container->singleton(StreamFactoryInterface::class,
|
||||||
|
fn(Container $c) => $c->get(Psr17Factory::class)
|
||||||
|
);
|
||||||
|
//$container->factory(ResponseInterface);
|
||||||
|
$container->singleton(RcloneClient::class, function (Container $c) {
|
||||||
|
return new RcloneClient(
|
||||||
|
http: $c->get(ClientInterface::class),
|
||||||
|
requests: $c->get(RequestFactoryInterface::class),
|
||||||
|
streams: $c->get(StreamFactoryInterface::class),
|
||||||
|
baseUrl: $_ENV['RCLONE_URL'] ?? 'http://rclone:5572',
|
||||||
|
user: $_ENV['RCLONE_USER'],
|
||||||
|
pass: $_ENV['RCLONE_PASS'],
|
||||||
|
);
|
||||||
|
});
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// Routes
|
// Routes
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -108,6 +139,9 @@ $routes = static function (RouteCollector $r): void {
|
|||||||
|
|
||||||
$r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']);
|
$r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']);
|
||||||
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
||||||
|
|
||||||
|
$r->get('/icloud/connect', [ICloudAuthController::class, 'connectForm']);
|
||||||
|
$r->post('/icloud/connect', [ICloudAuthController::class, 'submit']);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
62
resources/views/login_icloud.php
Normal file
62
resources/views/login_icloud.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<div class="login-card">
|
||||||
|
<h1>Connect iCloud</h1>
|
||||||
|
|
||||||
|
<?php if (!empty($viewModel->error)) : ?>
|
||||||
|
<p class="error"><?= htmlspecialchars($viewModel->error) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="info-message">
|
||||||
|
<ol>
|
||||||
|
<li>Open <b>icloud.com</b> and log in</li>
|
||||||
|
<li>Complete 2FA</li>
|
||||||
|
<li>Open DevTools → Network</li>
|
||||||
|
<li>Copy <b>Request Header → Cookie</b></li>
|
||||||
|
<li>Copy <b>X-APPLE-WEBAUTH-HSA-TRUST</b> value</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" action="/icloud/connect" class="login-form">
|
||||||
|
<label for="apple_id">Apple ID</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="apple_id"
|
||||||
|
name="apple_id"
|
||||||
|
required
|
||||||
|
placeholder="yourname@icloud.com"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
placeholder="iCloud password"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label for="cookies">Cookies (full header)</label>
|
||||||
|
<textarea
|
||||||
|
id="cookies"
|
||||||
|
name="cookies"
|
||||||
|
rows="6"
|
||||||
|
required
|
||||||
|
placeholder="X-APPLE-WEBAUTH-HSA-TRUST=...; X-APPLE-WEBAUTH-USER=...;"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<label for="trust_token">Trust token</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="trust_token"
|
||||||
|
name="trust_token"
|
||||||
|
required
|
||||||
|
placeholder="X-APPLE-WEBAUTH-HSA-TRUST value"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Connect iCloud
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf"
|
||||||
|
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
137
src/Controllers/ICloudAuthController.php
Normal file
137
src/Controllers/ICloudAuthController.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\Controllers;
|
||||||
|
|
||||||
|
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
||||||
|
use Din9xtrCloud\Rclone\RcloneClient;
|
||||||
|
use Din9xtrCloud\Repositories\IcloudAccountRepository;
|
||||||
|
use Din9xtrCloud\View;
|
||||||
|
use Din9xtrCloud\ViewModels\Icloud\ICloudLoginViewModel;
|
||||||
|
use Psr\Http\Message\ResponseFactoryInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final readonly class ICloudAuthController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ResponseFactoryInterface $responseFactory,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
private IcloudAccountRepository $repository,
|
||||||
|
private RcloneClient $rclone,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function connectForm(ServerRequestInterface $request): string
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = $_SESSION['icloud_error'] ?? '';
|
||||||
|
unset($_SESSION['icloud_error']);
|
||||||
|
|
||||||
|
return View::display(
|
||||||
|
new ICloudLoginViewModel(
|
||||||
|
title: 'Connect iCloud',
|
||||||
|
csrf: CsrfMiddleware::generateToken($request),
|
||||||
|
error: $error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function submit(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$data = (array)$request->getParsedBody();
|
||||||
|
$user = $request->getAttribute('user');
|
||||||
|
|
||||||
|
$appleId = trim((string)($data['apple_id'] ?? ''));
|
||||||
|
$password = trim((string)($data['password'] ?? ''));
|
||||||
|
$cookies = trim((string)($data['cookies'] ?? ''));
|
||||||
|
$trustToken = trim((string)($data['trust_token'] ?? ''));
|
||||||
|
|
||||||
|
if ($appleId === '' || $password === '' || $cookies === '' || $trustToken === '') {
|
||||||
|
return $this->fail('All fields are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$remote = 'icloud_' . $user->id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$encryptedAppleId = encryptString($appleId);
|
||||||
|
$encryptedPassword = encryptString($password);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->rclone->call('config/create', [
|
||||||
|
'name' => $remote,
|
||||||
|
'type' => 'iclouddrive',
|
||||||
|
'parameters' => [
|
||||||
|
'apple_id' => $appleId,
|
||||||
|
'password' => $password,
|
||||||
|
'cookies' => $cookies,
|
||||||
|
'trust_token' => $trustToken,
|
||||||
|
],
|
||||||
|
'nonInteractive' => true,
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->warning('iCloud remote exists, updating', [
|
||||||
|
'remote' => $remote,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->rclone->call('config/update', [
|
||||||
|
'name' => $remote,
|
||||||
|
'parameters' => [
|
||||||
|
'apple_id' => $appleId,
|
||||||
|
'password' => $password,
|
||||||
|
'cookies' => $cookies,
|
||||||
|
'trust_token' => $trustToken,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// health-check
|
||||||
|
$list = $this->rclone->call('operations/list', [
|
||||||
|
'fs' => $remote . ':',
|
||||||
|
'remote' => '',
|
||||||
|
'maxDepth' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logger->info('icloud list', $list);
|
||||||
|
|
||||||
|
$this->repository->createOrUpdate(
|
||||||
|
userId: $user->id,
|
||||||
|
remoteName: $remote,
|
||||||
|
appleId: $encryptedAppleId,
|
||||||
|
password: $encryptedPassword,
|
||||||
|
trustToken: $trustToken,
|
||||||
|
cookies: $cookies,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->responseFactory
|
||||||
|
->createResponse(302)
|
||||||
|
->withHeader('Location', '/storage');
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->error('iCloud connect failed', [
|
||||||
|
'remote' => $remote,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function fail(string $message): ResponseInterface
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
$_SESSION['icloud_error'] = $message;
|
||||||
|
|
||||||
|
return $this->responseFactory
|
||||||
|
->createResponse(302)
|
||||||
|
->withHeader('Location', '/icloud/connect');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Random\RandomException;
|
||||||
|
|
||||||
if (!function_exists('formatBytes')) {
|
if (!function_exists('formatBytes')) {
|
||||||
|
|
||||||
function formatBytes(int $bytes): string
|
function formatBytes(int $bytes): string
|
||||||
@@ -88,9 +90,54 @@ if (!function_exists('validateIp')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
|
$flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
|
||||||
|
|
||||||
return filter_var($ip, FILTER_VALIDATE_IP, $flags)
|
return filter_var($ip, FILTER_VALIDATE_IP, $flags)
|
||||||
? $ip
|
? $ip
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('encryptString')) {
|
||||||
|
/**
|
||||||
|
* XSalsa20 + Poly1305
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
function encryptString(string $plaintext): string
|
||||||
|
{
|
||||||
|
$key = hex2bin($_ENV['APP_KEY']); // 32 байта
|
||||||
|
if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||||
|
throw new RuntimeException('APP_KEY must be 32 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
|
||||||
|
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
|
||||||
|
|
||||||
|
return base64_encode($nonce . $ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('decryptString')) {
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
function decryptString(string $encrypted): string
|
||||||
|
{
|
||||||
|
$key = hex2bin($_ENV['APP_KEY']);
|
||||||
|
if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||||
|
throw new RuntimeException('APP_KEY must be 32 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = base64_decode($encrypted, true);
|
||||||
|
$nonce = substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
$ciphertext = substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
|
||||||
|
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
|
||||||
|
|
||||||
|
if ($plaintext === false) {
|
||||||
|
throw new RuntimeException('Failed to decrypt data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plaintext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
25
src/Models/IcloudAccount.php
Normal file
25
src/Models/IcloudAccount.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\Models;
|
||||||
|
|
||||||
|
final readonly class IcloudAccount
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public int $userId,
|
||||||
|
public string $remoteName,
|
||||||
|
|
||||||
|
public string $appleId,
|
||||||
|
public string $password,
|
||||||
|
|
||||||
|
public string $trustToken,
|
||||||
|
public string $cookies,
|
||||||
|
|
||||||
|
public string $status,
|
||||||
|
public int $connectedAt,
|
||||||
|
public int $createdAt,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Rclone/RcloneClient.php
Normal file
67
src/Rclone/RcloneClient.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\Rclone;
|
||||||
|
|
||||||
|
use JsonException;
|
||||||
|
use Psr\Http\Client\ClientExceptionInterface;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
use Psr\Http\Message\RequestFactoryInterface;
|
||||||
|
use Psr\Http\Message\StreamFactoryInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
final readonly class RcloneClient
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ClientInterface $http,
|
||||||
|
private RequestFactoryInterface $requests,
|
||||||
|
private StreamFactoryInterface $streams,
|
||||||
|
private string $baseUrl,
|
||||||
|
private string $user,
|
||||||
|
private string $pass,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method rc endpoint without
|
||||||
|
* @param array $params JSON body
|
||||||
|
* @return array
|
||||||
|
* @throws JsonException
|
||||||
|
* @throws ClientExceptionInterface
|
||||||
|
*/
|
||||||
|
public function call(string $method, array $params = []): array
|
||||||
|
{
|
||||||
|
$url = rtrim($this->baseUrl, '/') . '/' . ltrim($method, '/');
|
||||||
|
|
||||||
|
$body = $this->streams->createStream(
|
||||||
|
json_encode($params, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
|
||||||
|
$request = $this->requests
|
||||||
|
->createRequest('POST', $url)
|
||||||
|
->withHeader(
|
||||||
|
'Authorization',
|
||||||
|
'Basic ' . base64_encode($this->user . ':' . $this->pass)
|
||||||
|
)
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withBody($body);
|
||||||
|
|
||||||
|
$response = $this->http->sendRequest($request);
|
||||||
|
|
||||||
|
$status = $response->getStatusCode();
|
||||||
|
$content = (string)$response->getBody();
|
||||||
|
|
||||||
|
if ($status >= 400) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
sprintf('Rclone API error %d: %s', $status, $content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($content === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/Repositories/IcloudAccountRepository.php
Normal file
116
src/Repositories/IcloudAccountRepository.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\Repositories;
|
||||||
|
|
||||||
|
use Din9xtrCloud\Models\IcloudAccount;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final readonly class IcloudAccountRepository
|
||||||
|
{
|
||||||
|
public function __construct(private PDO $db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createOrUpdate(
|
||||||
|
int $userId,
|
||||||
|
string $remoteName,
|
||||||
|
string $appleId,
|
||||||
|
string $password,
|
||||||
|
string $trustToken,
|
||||||
|
string $cookies
|
||||||
|
): IcloudAccount
|
||||||
|
{
|
||||||
|
$existing = $this->findByUserId($userId);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
UPDATE icloud_accounts
|
||||||
|
SET apple_id = :apple_id,
|
||||||
|
password = :password,
|
||||||
|
trust_token = :trust_token,
|
||||||
|
cookies = :cookies,
|
||||||
|
status = 'connected',
|
||||||
|
connected_at = :connected_at
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':apple_id' => $appleId,
|
||||||
|
':password' => $password,
|
||||||
|
':trust_token' => $trustToken,
|
||||||
|
':cookies' => $cookies,
|
||||||
|
':connected_at' => $now,
|
||||||
|
':id' => $existing->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new IcloudAccount(
|
||||||
|
id: $existing->id,
|
||||||
|
userId: $userId,
|
||||||
|
remoteName: $remoteName,
|
||||||
|
appleId: $appleId,
|
||||||
|
password: $password,
|
||||||
|
trustToken: $trustToken,
|
||||||
|
cookies: $cookies,
|
||||||
|
status: 'connected',
|
||||||
|
connectedAt: $now,
|
||||||
|
createdAt: $existing->createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
INSERT INTO icloud_accounts
|
||||||
|
(user_id, remote_name, apple_id, password, trust_token, cookies, status, connected_at, created_at)
|
||||||
|
VALUES
|
||||||
|
(:user_id, :remote_name, :apple_id, :password, :trust_token, :cookies, 'connected', :connected_at, :created_at)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':user_id' => $userId,
|
||||||
|
':remote_name' => $remoteName,
|
||||||
|
':apple_id' => $appleId,
|
||||||
|
':password' => $password,
|
||||||
|
':trust_token' => $trustToken,
|
||||||
|
':cookies' => $cookies,
|
||||||
|
':connected_at' => $now,
|
||||||
|
':created_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new IcloudAccount(
|
||||||
|
id: (int)$this->db->lastInsertId(),
|
||||||
|
userId: $userId,
|
||||||
|
remoteName: $remoteName,
|
||||||
|
appleId: $appleId,
|
||||||
|
password: $password,
|
||||||
|
trustToken: $trustToken,
|
||||||
|
cookies: $cookies,
|
||||||
|
status: 'connected',
|
||||||
|
connectedAt: $now,
|
||||||
|
createdAt: $now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUserId(int $userId): ?IcloudAccount
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT * FROM icloud_accounts WHERE user_id = :user_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':user_id' => $userId]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return $row ? new IcloudAccount(
|
||||||
|
id: (int)$row['id'],
|
||||||
|
userId: (int)$row['user_id'],
|
||||||
|
remoteName: $row['remote_name'],
|
||||||
|
appleId: $row['apple_id'],
|
||||||
|
password: $row['password'],
|
||||||
|
trustToken: $row['trust_token'],
|
||||||
|
cookies: $row['cookies'],
|
||||||
|
status: $row['status'],
|
||||||
|
connectedAt: (int)$row['connected_at'],
|
||||||
|
createdAt: (int)$row['created_at'],
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/Storage/Drivers/ICloudStorageDriver.php
Normal file
104
src/Storage/Drivers/ICloudStorageDriver.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\Storage\Drivers;
|
||||||
|
|
||||||
|
use Din9xtrCloud\Rclone\RcloneClient;
|
||||||
|
use JsonException;
|
||||||
|
use Psr\Http\Client\ClientExceptionInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Psr\Http\Message\UploadedFileInterface;
|
||||||
|
|
||||||
|
final readonly class ICloudStorageDriver implements StorageDriverInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RcloneClient $rclone,
|
||||||
|
private string $remoteName,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ClientExceptionInterface
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function getUsedBytes(int|string $userId): int
|
||||||
|
{
|
||||||
|
$stats = $this->rclone->call('operations/about', [
|
||||||
|
'fs' => $this->remoteName . ':',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (int)($stats['used'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ClientExceptionInterface
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
public function createFolder(int|string $userId, string $name): void
|
||||||
|
{
|
||||||
|
$this->rclone->call('operations/mkdir', [
|
||||||
|
'fs' => $this->remoteName . ':',
|
||||||
|
'remote' => $name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalBytes(int|string $userId): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getUsedBytesByCategory(int|string $userId): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeUploadedFile(int|string $userId, string $folder, UploadedFileInterface $file, string $targetName): void
|
||||||
|
{
|
||||||
|
// TODO: Implement storeUploadedFile() method.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserFolderPath(int|string $userId, string $folder): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function tusInit(int|string $userId, string $uploadId, int $size, array $metadata): void
|
||||||
|
{
|
||||||
|
// TODO: Implement tusInit() method.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tusWriteChunk(int|string $userId, string $uploadId, int $offset, StreamInterface $stream): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function tusGetStatus(int|string $userId, string $uploadId): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserPath(int|string $userId): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilePath(int|string $userId, string $folder, string $filename): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteFile(int|string $userId, string $folder, string $filename): void
|
||||||
|
{
|
||||||
|
// TODO: Implement deleteFile() method.
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/ViewModels/Icloud/ICloudLoginViewModel.php
Normal file
35
src/ViewModels/Icloud/ICloudLoginViewModel.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Din9xtrCloud\ViewModels\Icloud;
|
||||||
|
|
||||||
|
use Din9xtrCloud\ViewModels\BaseViewModel;
|
||||||
|
use Din9xtrCloud\ViewModels\LayoutConfig;
|
||||||
|
|
||||||
|
final readonly class ICloudLoginViewModel extends BaseViewModel
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
string $title,
|
||||||
|
public ?string $csrf = null,
|
||||||
|
public bool $show2fa = false,
|
||||||
|
public string $error = '',
|
||||||
|
public string $appleId = ''
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$layoutConfig = new LayoutConfig(
|
||||||
|
header: null,
|
||||||
|
showFooter: true,
|
||||||
|
);
|
||||||
|
parent::__construct($layoutConfig, $title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function template(): string
|
||||||
|
{
|
||||||
|
return 'login_icloud';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user