feat: add iCloud integration
This commit is contained in:
@@ -33,7 +33,8 @@
|
||||
"relay/relay": "^3.0",
|
||||
"ext-pdo": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-zip": "*"
|
||||
"ext-zip": "*",
|
||||
"php-http/curl-client": "^2.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
@@ -59,7 +60,8 @@
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true
|
||||
"phpstan/extension-installer": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
db/migration/005_create_icloud_accounts.php
Normal file
21
db/migration/005_create_icloud_accounts.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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,
|
||||
trust_token TEXT,
|
||||
cookies TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
connected_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
");
|
||||
};
|
||||
@@ -12,16 +12,40 @@ services:
|
||||
- ./db:/var/db
|
||||
- ./php/php.ini:/usr/local/etc/php/conf.d/uploads.ini
|
||||
- ./Caddyfile:/etc/frankenphp/Caddyfile
|
||||
# - caddy_data:/data
|
||||
# - caddy_config:/config
|
||||
# - caddy_data:/data
|
||||
# - caddy_config:/config
|
||||
env_file:
|
||||
- .env
|
||||
tty: false
|
||||
networks:
|
||||
- internal
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-kf", "http://localhost/" ]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
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:
|
||||
# caddy_data:
|
||||
# caddy_config:
|
||||
|
||||
@@ -48,7 +48,7 @@ opcache.enable_cli = 1
|
||||
opcache.memory_consumption = 256
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.validate_timestamps = 1
|
||||
opcache.revalidate_freq = 2
|
||||
opcache.max_wasted_percentage = 50
|
||||
; JIT
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use Din9xtrCloud\Controllers\ICloudAuthController;
|
||||
use Din9xtrCloud\Rclone\RcloneClient;
|
||||
use Http\Client\Curl\Client;
|
||||
use Din9xtrCloud\App;
|
||||
use Din9xtrCloud\Container\Container;
|
||||
use Din9xtrCloud\Controllers\AuthController;
|
||||
@@ -23,7 +26,11 @@ use Monolog\Processor\PsrLogMessageProcessor;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Monolog\Level;
|
||||
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\StreamFactoryInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -77,6 +84,30 @@ $container->request(
|
||||
)->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
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -108,6 +139,10 @@ $routes = static function (RouteCollector $r): void {
|
||||
|
||||
$r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']);
|
||||
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
||||
|
||||
$r->get('/icloud/connect', [ICloudAuthController::class, 'connectForm']);
|
||||
$r->post('/icloud/connect', [ICloudAuthController::class, 'submitCredentials']);
|
||||
$r->post('/icloud/2fa', [ICloudAuthController::class, 'submit2fa']);
|
||||
};
|
||||
|
||||
|
||||
|
||||
114
resources/views/login_icloud.php
Normal file
114
resources/views/login_icloud.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<!-- icloud_login.php -->
|
||||
<div class="login-card">
|
||||
<h1><?= htmlspecialchars($viewModel->title ?? 'iCloud Login') ?></h1>
|
||||
|
||||
<?php if (!empty($viewModel->error)) : ?>
|
||||
<p class="error"><?= htmlspecialchars($viewModel->error) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($viewModel->show2fa) && $viewModel->show2fa): ?>
|
||||
<div class="info-message" style="
|
||||
background: rgba(66, 153, 225, 0.1);
|
||||
border-left: 4px solid #4299e1;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: #2d3748;
|
||||
">
|
||||
<p style="margin: 0; font-weight: 500;">
|
||||
Enter the 6-digit verification code sent to your trusted devices.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/icloud/2fa" class="login-form">
|
||||
<input type="hidden" name="apple_id" value="<?= htmlspecialchars($viewModel->appleId ?? '') ?>">
|
||||
<input type="hidden" name="password" value="<?= htmlspecialchars($viewModel->password ?? '') ?>">
|
||||
|
||||
<label for="code">
|
||||
6-digit verification code
|
||||
</label>
|
||||
<input type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6"
|
||||
required
|
||||
placeholder="123456"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
style="letter-spacing: 2px; font-size: 1.2rem; text-align: center;">
|
||||
|
||||
<button type="submit" style="margin-top: 1rem;">
|
||||
Verify Code
|
||||
</button>
|
||||
|
||||
<div style="margin-top: 1rem; text-align: center;">
|
||||
<a href="/icloud/connect" style="color: #667eea; text-decoration: none;">
|
||||
← Back to login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf"
|
||||
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
||||
</form>
|
||||
|
||||
<?php else: ?>
|
||||
<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="name@example.com"
|
||||
autocomplete="username">
|
||||
|
||||
<label for="password">
|
||||
Password
|
||||
</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
autocomplete="current-password">
|
||||
|
||||
<button type="submit">
|
||||
Connect iCloud
|
||||
</button>
|
||||
|
||||
<input type="hidden" name="_csrf"
|
||||
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const codeInput = document.getElementById('code');
|
||||
if (codeInput) {
|
||||
codeInput.addEventListener('keypress', function (e) {
|
||||
const char = String.fromCharCode(e.which);
|
||||
if (!/^\d$/.test(char)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
codeInput.addEventListener('input', function () {
|
||||
if (this.value.length === 6) {
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.info-message {
|
||||
background: rgba(66, 153, 225, 0.15) !important;
|
||||
border-left-color: #4299e1 !important;
|
||||
color: #cbd5e1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
152
src/Controllers/ICloudAuthController.php
Normal file
152
src/Controllers/ICloudAuthController.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Controllers;
|
||||
|
||||
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
||||
use Din9xtrCloud\Rclone\RcloneClient;
|
||||
use Din9xtrCloud\Rclone\RcloneICloudConfigurator;
|
||||
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 RcloneICloudConfigurator $configurator,
|
||||
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();
|
||||
}
|
||||
|
||||
$query = $request->getQueryParams();
|
||||
$show2fa = (bool)($query['show2fa'] ?? false);
|
||||
$appleId = (string)($query['apple_id'] ?? '');
|
||||
|
||||
$error = '';
|
||||
if (isset($_SESSION['icloud_error'])) {
|
||||
$error = (string)$_SESSION['icloud_error'];
|
||||
unset($_SESSION['icloud_error']);
|
||||
}
|
||||
|
||||
$title = $show2fa ? 'iCloud 2FA Verification' : 'iCloud Login';
|
||||
|
||||
return View::display(
|
||||
new ICloudLoginViewModel(
|
||||
title: $title,
|
||||
csrf: CsrfMiddleware::generateToken($request),
|
||||
show2fa: $show2fa,
|
||||
error: $error,
|
||||
appleId: $appleId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function submitCredentials(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$data = (array)$request->getParsedBody();
|
||||
$user = $request->getAttribute('user');
|
||||
|
||||
$appleId = (string)($data['apple_id'] ?? '');
|
||||
$password = (string)($data['password'] ?? '');
|
||||
$remote = 'icloud_' . $user->id;
|
||||
|
||||
try {
|
||||
$this->configurator->createRemote($remote, $appleId);
|
||||
$this->configurator->setPassword($remote, $password);
|
||||
|
||||
$config = $this->configurator->getConfig($remote);
|
||||
$trustToken = $config['trust_token'] ?? null;
|
||||
$cookies = $config['cookies'] ?? null;
|
||||
|
||||
$this->repository->create(
|
||||
$user->id,
|
||||
$remote,
|
||||
$appleId,
|
||||
$trustToken,
|
||||
$cookies
|
||||
);
|
||||
|
||||
return $this->responseFactory
|
||||
->createResponse(302)
|
||||
->withHeader('Location', '/icloud/connect?show2fa=1&apple_id=' . urlencode($appleId));
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$_SESSION['icloud_error'] = $e->getMessage();
|
||||
|
||||
return $this->responseFactory
|
||||
->createResponse(302)
|
||||
->withHeader('Location', '/icloud/connect');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function submit2fa(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$data = (array)$request->getParsedBody();
|
||||
$user = $request->getAttribute('user');
|
||||
|
||||
$code = (string)($data['code'] ?? '');
|
||||
$appleId = (string)($data['apple_id'] ?? '');
|
||||
$remote = 'icloud_' . $user->id;
|
||||
|
||||
try {
|
||||
$this->configurator->submit2fa($remote, $code);
|
||||
|
||||
$account = $this->repository->findByUserId($user->id);
|
||||
if ($account) {
|
||||
$this->repository->update($account, [
|
||||
'status' => 'connected',
|
||||
'connected_at' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
$contents = $this->rclone->call('operations/list', [
|
||||
'fs' => $remote . ':',
|
||||
'remote' => '',
|
||||
'opt' => [],
|
||||
'recurse' => true,
|
||||
'dirsOnly' => false,
|
||||
'filesOnly' => false,
|
||||
'metadata' => true,
|
||||
]);
|
||||
|
||||
$this->logger->info('iCloud contents', ['user' => $user->id, '$contents' => $contents]);
|
||||
|
||||
return $this->responseFactory
|
||||
->createResponse(302)
|
||||
->withHeader('Location', '/storage');
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
$_SESSION['icloud_error'] = $e->getMessage();
|
||||
|
||||
return $this->responseFactory
|
||||
->createResponse(302)
|
||||
->withHeader('Location', '/icloud/connect?' . http_build_query([
|
||||
'show2fa' => 1,
|
||||
'apple_id' => $appleId
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
src/Models/IcloudAccount.php
Normal file
21
src/Models/IcloudAccount.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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 $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);
|
||||
}
|
||||
}
|
||||
83
src/Rclone/RcloneICloudConfigurator.php
Normal file
83
src/Rclone/RcloneICloudConfigurator.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Din9xtrCloud\Rclone;
|
||||
|
||||
use JsonException;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
|
||||
final readonly class RcloneICloudConfigurator
|
||||
{
|
||||
public function __construct(
|
||||
private RcloneClient $rclone,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function createRemote(
|
||||
string $remote,
|
||||
string $appleId
|
||||
): void
|
||||
{
|
||||
$this->rclone->call('config/create', [
|
||||
'name' => $remote,
|
||||
'type' => 'iclouddrive',
|
||||
'parameters' => [
|
||||
'apple_id' => $appleId,
|
||||
],
|
||||
'opt' => [
|
||||
'nonInteractive' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function setPassword(
|
||||
string $remote,
|
||||
string $password
|
||||
): void
|
||||
{
|
||||
$this->rclone->call('config/password', [
|
||||
'name' => $remote,
|
||||
'parameters' => [
|
||||
'password' => $password,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function submit2fa(
|
||||
string $remote,
|
||||
string $code
|
||||
): void
|
||||
{
|
||||
$this->rclone->call('config/update', [
|
||||
'name' => $remote,
|
||||
'parameters' => [
|
||||
'config_2fa' => $code,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function getConfig(string $remote): array
|
||||
{
|
||||
return $this->rclone->call('config/show', [
|
||||
'name' => $remote
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
83
src/Repositories/IcloudAccountRepository.php
Normal file
83
src/Repositories/IcloudAccountRepository.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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 create(
|
||||
int $userId,
|
||||
string $remoteName,
|
||||
string $appleId,
|
||||
?string $trustToken = null,
|
||||
?string $cookies = null
|
||||
): IcloudAccount
|
||||
{
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO icloud_accounts (user_id, remote_name, apple_id, trust_token, cookies)
|
||||
VALUES (:user_id, :remote_name, :apple_id, :trust_token, :cookies)
|
||||
");
|
||||
$stmt->execute([
|
||||
':user_id' => $userId,
|
||||
':remote_name' => $remoteName,
|
||||
':apple_id' => $appleId,
|
||||
':trust_token' => $trustToken,
|
||||
':cookies' => $cookies,
|
||||
]);
|
||||
|
||||
$id = (int)$this->db->lastInsertId();
|
||||
|
||||
return new IcloudAccount(
|
||||
id: $id,
|
||||
userId: $userId,
|
||||
remoteName: $remoteName,
|
||||
appleId: $appleId,
|
||||
trustToken: $trustToken,
|
||||
cookies: $cookies,
|
||||
status: 'pending',
|
||||
connectedAt: null,
|
||||
createdAt: time(),
|
||||
);
|
||||
}
|
||||
|
||||
public function update(
|
||||
IcloudAccount $account,
|
||||
array $fields
|
||||
): void
|
||||
{
|
||||
$set = [];
|
||||
$params = [':id' => $account->id];
|
||||
foreach ($fields as $key => $value) {
|
||||
$set[] = "$key = :$key";
|
||||
$params[":$key"] = $value;
|
||||
}
|
||||
$stmt = $this->db->prepare("UPDATE icloud_accounts SET " . implode(', ', $set) . " WHERE id = :id");
|
||||
$stmt->execute($params);
|
||||
}
|
||||
|
||||
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'],
|
||||
trustToken: $row['trust_token'],
|
||||
cookies: $row['cookies'],
|
||||
status: $row['status'],
|
||||
connectedAt: $row['connected_at'] ? (int)$row['connected_at'] : null,
|
||||
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