Depolama Modülü Yazma
Altı aktarım metodu uygulayarak yeni bir yedek hedefi ekleyin. Operatör o hedefte istediği kadar hesap kaydedebilir.
Genel Bakış
Depolama modülü, yedek arşivlerinin gittiği yerdir. Küresel bir seçim değildir: operatör hedefleri satır satır kaydeder. Kimlik bilgileri config.php içinde durmaz; hedef başına, çözülmüş gelir.
İki taban sınıfı vardır. StorageModule operatörün yazdığı kimlik bilgileriyle bağlanır; CloudStorageModule OAuth yetkilendirme kodu akışını ekler.
Altı modül gelir. Başarısızlık bir StorageException fırlatılarak bildirilir; başarı hiçbir şey döndürmez.
Ön Koşullar
- Büyük dosya saklayan, geri okuyan, dizin listeleyen ve bayt boyutu bildiren bir hedef.
- Bulut sağlayıcı için client id, secret ve yönlendirme adresi.
- Modül Anatomisi ve Modül Yapılandırması.
Yapı
coremio/modules/Storage/AcmeVault/
├── AcmeVault.php class AcmeVault extends StorageModule (ya da CloudStorageModule)
├── config.php ['meta' => [...], 'defaults' => [...]] kimlik bilgisi burada tutulmaz
└── lang/
├── en.php
└── tr.php
StorageConnectionException ve StorageAuthException daraltır.
encryptedFields() ile adlandırılan alanları çözer; encodeStorageConfig() kayıtta şifreler.
Adım Adım
1. Taban Sınıfı Seçin
- Operatörün yazdığı kimlik bilgileri varsa
StorageModulegenişletin, FTP gibi. - Operatör oturum açıyor ve siz yenileme jetonu tutuyorsanız
CloudStorageModulegenişletin, GoogleDrive gibi. - Bağlan butonu yalnız bulut alt sınıfında görünür.
2. Formu ve Sırları Bildirin
configuration()uygulayın. Her alan adı config dizisinin anahtarı olur.- Statik
encryptedFields()uygulayın ve her sırrı adlandırın. Unuttuğunuz alan düz metin saklanır. - Sır olmayan varsayılanları
config.phpiçindekidefaultsaltına koyun; satır üzerine birleştirilir.
3. Aktarım Metotlarını Uygulayın
test()yazılabilirliği kanıtlamalı: yoklama dosyası yükleyin, boyutunu okuyun, silin.upload(),download(),delete(),list(),ensureDirectory()yazın. Hepsi void döner, hatada fırlatır.- Sağlayıcınızda ucuz bir boyut çağrısı varsa
remoteSize()geçersiz kılın; varsayılan onulist()üzerinden türetir. - Uzun aktarımlara ilerleme geri çağrısını bağlayın.
4. OAuth Yarısını Ekleyin
authorizationUrl(),exchangeCode(),refreshToken()verevokeTokens()uygulayın.- Geri çağrı rotası yazmayın. Ortak işleyici imzalı durumu doğrular,
exchangeCode()çağırır ve sonucu geri gönderir. - Jeton alanlarını
configuration()içinde gizli kayıt olarak bildirin: erişim jetonu, yenileme jetonu, bitiş zamanı, hesap. - Tembel yenileyin: her çağrıdan önce bitiş zamanına bakın.
Referans
StorageModule
// Config hedef başına gelir: varsayılanlar, satırın çözülmüş kimlik bilgileriyle birleşmiş hâlde.
public function __construct(array $storageConfig = []);
// ── soyut: beşini de uygulamak zorundasınız ────────────────────────────
abstract public function test(): void;
abstract public function upload(string $localPath, string $remoteName): void;
abstract public function download(string $remoteName, string $localPath): void;
abstract public function delete(string $remoteName): void;
abstract public function ensureDirectory(string $path): void;
// Her kayıt: ['name' => string, 'size' => int, 'mtime' => int|null]
abstract public function list(string $prefix = ''): array;
// ── sanal: makul varsayılanlar, daha iyisini yapabiliyorsanız ezin ─────
public function remoteSize(string $remoteName): ?int; // list() üzerinden türetilir
public function downloadStream(string $remoteName, $outputStream): void; // geçici dosya üzerinden
public function getPresignedDownloadUrl(string $remoteName, int $ttlSeconds = 300): ?string; // null
public static function encryptedFields(): array; // []
public function configuration(): array; // []
public function controller_test(): array; // test() metodunu try/catch içine sarar
// ── devralınan tesisat, yeniden uygulamayın ────────────────────────────
public function setProgressCallback(?callable $cb): void;
public function getProgressCallback(): ?callable;
protected function attachProgressCallback(\CurlHandle $ch, ?int $intervalSec = null): void;
const DEFAULT_FOLDER_PATH = '/wisecp-backup';
const HEARTBEAT_INTERVAL_SEC = 5;
test() çağırıp formun beklediği biçimi döndürür.
CloudStorageModule
// $state değerini generateSignedState() üretir ve sağlayıcıya aynen iletilmelidir.
abstract public function authorizationUrl(string $state): string;
// Şunu döndürür: ['access_token' => string, 'refresh_token' => string,
// 'expires_at' => int, 'account_email' => ?string]
// Hatada StorageAuthException fırlatır.
abstract public function exchangeCode(string $code): array;
// $this->config['access_token'] ve ['expires_at'] değerlerini yerinde değiştirir.
abstract public function refreshToken(): void;
// Varsayılanı boş işlemdir. Sağlayıcının iptal ucuna gitmek için ezin.
public function revokeTokens(string $refreshToken): void;
// Zaten uygulanmış: durumu doğrular, exchangeCode() çağırır, popup yükünü kurar.
public function callback_handle(): array;
// Geçici kimlik bilgilerini taşıyan HMAC imzalı durum; böylece geri çağrı ne oturuma
// ne de veritabanı sorgusuna ihtiyaç duyar. Varsayılan ömrü on dakikadır.
public static function generateSignedState(array $cfg = [], int $ttlSeconds = 600): string;
public static function decodeSignedState(string $state): array|false;
Geri çağrı rotası coremio/modules/Storage/router.php içindedir; slug'ı modül adına eşler. Yeni sağlayıcı için bir kayıt yeter:
// api/system/backup/{slug}/callback
$providers = ['google-drive' => 'GoogleDrive', 'onedrive' => 'OneDrive', 'yandex-disk' => 'YandexDisk'];
$module = Modules::getInstance("Storage", $providers[$slug], [[]]);
$result = $module->callback_handle(); // {status: connected|error, access_token, refresh_token, ...}
// Sonuç, popup'ı açan pencereye geri gönderilir.
configuration() Tanımı
Düz bir dizi listesi; yalnız type ve name zorunludur.
| Anahtar | Kabul ettiği | Ne yapar |
|---|---|---|
type | text, number, password, checkbox, select, hidden, section, redirect_uri, oauth_connect | Kontrolü seçer. Son üçü yerleşimdir: başlık, adres, oturum butonu. |
name | dize | Config anahtarı; $this->config['name'] olarak okunur. |
label | dize | Görünen etiket; $this->lang üzerinden düz bir yedekle okuyun. |
width | 1 ile 12 arası | Izgara sütunu. |
required | bool | Alanı zorunlu işaretler. |
encrypted | bool | Arayüz için sır işaretidir. Hiçbir şeyi şifrelemez: şifreleme encryptedFields() ile olur, ikisi uyuşmalıdır. |
value, checked, placeholder, description | karışık | Başlangıç değeri, işaret durumu, ipucu, yardım metni. |
step, doc_url, doc_label | int, dize, dize | section kaydında: numarası ve konsol bağlantısı. |
Kimlik Bilgileri Nerede Durur
type, şifreli JSON config.
meta ve defaults okur, sonra geçirilen config'i üzerine $this->config içinde birleştirir.
Modules::getInstance("Storage", $type, [$config]). Sarmalama yapıcının argüman listesidir.
test() metodunu yalnız folder_name, folder_path, base_path ya da remote_directory değiştiyse tekrar çalıştırır.
Yükleme Doğrulaması
Yedek motoru upload() metodundan temiz dönmeye güvenmez.
$module = Backup::buildStorage($storageId); // satır -> çöz -> getInstance
// Kalp atışı: yoksa yavaş bir yükleme kuyruğun bayatlama penceresini aşar ve sonlandırılır.
$module->setProgressCallback(function ($bytes = 0, $total = 0) use ($queueId, $backupId, $row): void {
if ($queueId > 0) CronJobQueue::touch($queueId);
$tot = (int) ($total > 0 ? $total : ($row['file_size'] ?? 0));
$pct = $tot > 0 ? (int) min(100, round(((int) $bytes) * 100 / $tot)) : 0;
Backup::updateProgress($backupId, 'upload', $pct, (int) $bytes, $tot);
});
$module->upload($finalArchive, $finalName);
// Sonra: baytların indiğini kanıtlayın. Temiz dönüş kanıt değildir; kesilen bir aktarım
// doğru adlı ama kırpılmış bir dosya bırakabilir ve bu ancak birinin yedeğe ihtiyacı
// olduğunda fark edilir. null "yanıtlayamadım" demektir ve başarısız değil doğrulanmamış kaydedilir.
$remoteSize = $module->remoteSize($finalName);
if ($remoteSize !== null && $remoteSize !== $localSize)
throw new Exception("Upload verification failed: the destination holds {$remoteSize} bytes.");
Örnek
Düz bir hedef modülü, sonra onu geri okuyan iki taraf.
<?php
namespace WISECP\Modules\Storage;
use StorageModule;
use StorageException;
use StorageConnectionException;
use Utility;
class AcmeVault extends StorageModule
{
// Burada adlandırılır, sınıfın dışında hedef satırına girerken şifrelenir.
// Bu listede olmayan bir sır düz metin olarak saklanır.
public static function encryptedFields(): array
{
return ['api_key'];
}
public function configuration(): array
{
return [
['type' => 'text', 'name' => 'bucket', 'width' => 8, 'label' => $this->lang['field-bucket'] ?? 'Bucket', 'required' => true],
['type' => 'text', 'name' => 'region', 'width' => 4, 'label' => $this->lang['field-region'] ?? 'Region', 'value' => 'eu-central'],
['type' => 'password', 'name' => 'api_key', 'width' => 12, 'label' => $this->lang['field-api-key'] ?? 'API Key', 'required' => true, 'encrypted' => true],
// Bir dizin alanına yol benzeri adlardan birini vermek, o alandaki düzenlemenin
// kayıtta test() metodunu yeniden koşturmasını sağlar. Diğerleri yeniden doğrulanmadan kaydedilir.
['type' => 'text', 'name' => 'remote_directory', 'width' => 12,
'label' => $this->lang['field-remote-directory'] ?? 'Remote directory',
'value' => self::DEFAULT_FOLDER_PATH, 'placeholder' => self::DEFAULT_FOLDER_PATH],
];
}
public function test(): void
{
$this->ensureDirectory('');
// Erişilebilirlik yazılabilirlik değildir. Bir yoklama yazın, boyutunu geri okuyun, silin.
$name = '.wisecp-storage-test-' . bin2hex(random_bytes(4));
$probe = 'wisecp';
$tmp = tempnam(sys_get_temp_dir(), 'wcp-acme-');
file_put_contents($tmp, $probe);
try {
$this->upload($tmp, $name);
$size = $this->remoteSize($name);
$this->delete($name);
// null sağlayıcının yanıtlayamadığı anlamına gelir, bu da hiçbir yönde kanıt değildir.
if ($size !== null && $size !== strlen($probe))
throw new StorageException(sprintf(
$this->lang['error-upload-incomplete'] ?? 'Upload incomplete: the server stored %s of %s bytes',
$size, strlen($probe)
));
}
finally {
@unlink($tmp);
}
}
public function upload(string $localPath, string $remoteName): void
{
if (!is_file($localPath)) throw new StorageException("Local file not found: {$localPath}");
$handle = fopen($localPath, 'rb');
if (!$handle) throw new StorageException("Local file not readable: {$localPath}");
$ch = curl_init($this->endpoint($remoteName));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_UPLOAD => true,
CURLOPT_INFILE => $handle,
CURLOPT_INFILESIZE => filesize($localPath),
CURLOPT_HTTPHEADER => $this->headers(),
]);
// Bu olmadan gözcü hiç etkinlik görmez ve yavaş bir yüklemeyi bayat sayıp sonlandırabilir.
$this->attachProgressCallback($ch);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
fclose($handle);
if ($err !== '') throw new StorageConnectionException($err);
if ($code < 200 || $code > 299) throw new StorageException("Upload failed (HTTP {$code}): " . (string) $body);
}
public function download(string $remoteName, string $localPath): void
{
$out = fopen($localPath, 'wb');
if (!$out) throw new StorageException("Cannot write to {$localPath}");
try { $this->downloadStream($remoteName, $out); }
finally { fclose($out); }
}
public function delete(string $remoteName): void
{
$this->request('DELETE', $remoteName);
}
public function list(string $prefix = ''): array
{
$data = Utility::jdecode($this->request('GET', $prefix), true);
$out = [];
foreach (($data['objects'] ?? []) as $item)
$out[] = [
'name' => (string) ($item['key'] ?? ''),
'size' => (int) ($item['bytes'] ?? 0),
'mtime' => isset($item['modified']) ? (int) strtotime((string) $item['modified']) : null,
];
return $out;
}
// Dizin listesinin tamamını okuyan devralınan sürümden daha ucuz.
public function remoteSize(string $remoteName): ?int
{
try { $head = Utility::jdecode($this->request('HEAD', $remoteName), true); }
catch (\Throwable $e) { return null; }
return isset($head['bytes']) ? (int) $head['bytes'] : null;
}
public function ensureDirectory(string $path): void
{
$this->request('MKCOL', $path);
}
private function endpoint(string $name): string
{
$base = trim((string) ($this->config['remote_directory'] ?? self::DEFAULT_FOLDER_PATH), '/');
return 'https://' . rawurlencode((string) ($this->config['region'] ?? '')) . '.acmevault.example/'
. rawurlencode((string) ($this->config['bucket'] ?? '')) . '/' . trim($base . '/' . $name, '/');
}
private function headers(): array
{
// Anahtar çözülmüş olarak gelir: çözme, satır okunurken yapıldı.
return ['X-Api-Key: ' . (string) ($this->config['api_key'] ?? '')];
}
private function request(string $method, string $name): string
{
$ch = curl_init($this->endpoint($name));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $this->headers(),
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err !== '') throw new StorageConnectionException($err);
if ($code < 200 || $code > 299) throw new StorageException("{$method} failed (HTTP {$code})");
return (string) $body;
}
}
<?php
return [
'meta' => [
'name' => 'AcmeVault',
'version' => '1.0',
'description' => 'Object storage over HTTPS',
'icon' => 'bi bi-hdd-rack',
],
// Hedef satırının ALTINA birleşir, yani satırdaki değer her zaman kazanır. Kimlik
// bilgileri burada asla yer almaz: onlar modüle değil hedefe aittir.
'defaults' => [
'region' => 'eu-central',
'remote_directory' => '',
],
];
// 1. Hedefler ekranı sağlayıcı listesini ve formunuzu modülün kendisinden kurar.
$module = Modules::getInstance("Storage", $name, [[]]); // boş config: yalnız üst veri
$entry = [
'key' => $name,
'name' => $module->lang['name'] ?? $name,
'description' => $module->lang['description'] ?? ($module->meta['description'] ?? ''),
'oauth' => is_subclass_of("WISECP\\Modules\\Storage\\{$name}", "CloudStorageModule"),
'fields' => $module->configuration(),
];
// 2. Kayıt tam olarak encryptedFields() metodunun adlandırdığı alanları şifreler, sonra JSON'u saklar.
WDB::insert("backup_storage", [
'name' => $displayName,
'type' => $name,
'config' => Backup::encodeStorageConfig($name, $config),
'status' => 'enabled',
]);
// 3. Yedek koşusu ters yöne gider: satırı oku, çöz, örneği kur, aktar.
$module = Backup::buildStorage($storageId);
$module->upload($finalArchive, $finalName);
Tuzaklar
Form alanındaki encrypted yalnız görünümü belirler; şifrelemeyi statik liste yürütür. Arıza sessizdir: değer veritabanında okunabilir durur.
remoteSize() "yanıtlayamadım" (null) ile "dosya boş" (sıfır) arasını ayırır. Yanıtlanamayan kontrole sıfır döndürmek sağlam bir arşivi çöpe atar.
Yüklemeler bayatlama penceresi olan kuyruk işlerinde çalışır; büyük aktarım pencereyi aşabilir. İlerleme geri çağrısı olmazsa iş yarıda kesilir.
Modules::getInstance("Storage", $type, [$config]) üçüncü argümanı yapıcının argüman listesidir; sarmalanmamış config değerlere dağılır.
Bir bulut hedefini silmek önce revokeTokens() çağırır; satır her hâlükârda kaldırılır. Uygulamazsanız sağlayıcıda çalışan bir yenileme jetonu kalır.
İlgili Makaleler
Geri bildiriminiz için teşekkürler!
Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.