Depolama Modülü Yazma

1.6k görüntülenme Markdown

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ı

dosya düzeni
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
StorageModule Düz taban: beş soyut metot ve geçersiz kılınabilir varsayılanlar.
CloudStorageModule Üç soyut OAuth metodu ve bir geri çağrı işleyicisi ekler.
StorageException Hata kanalı; StorageConnectionException ve StorageAuthException daraltır.
Backup::buildStorage() Hedef satırını örneğinize çevirir.
Backup::decodeStorageConfig() encryptedFields() ile adlandırılan alanları çözer; encodeStorageConfig() kayıtta şifreler.
coremio/modules/Storage Klasörünüz buraya girer.

Adım Adım

1. Taban Sınıfı Seçin

  1. Operatörün yazdığı kimlik bilgileri varsa StorageModule genişletin, FTP gibi.
  2. Operatör oturum açıyor ve siz yenileme jetonu tutuyorsanız CloudStorageModule genişletin, GoogleDrive gibi.
  3. Bağlan butonu yalnız bulut alt sınıfında görünür.

2. Formu ve Sırları Bildirin

  1. configuration() uygulayın. Her alan adı config dizisinin anahtarı olur.
  2. Statik encryptedFields() uygulayın ve her sırrı adlandırın. Unuttuğunuz alan düz metin saklanır.
  3. Sır olmayan varsayılanları config.php içindeki defaults altına koyun; satır üzerine birleştirilir.

3. Aktarım Metotlarını Uygulayın

  1. test() yazılabilirliği kanıtlamalı: yoklama dosyası yükleyin, boyutunu okuyun, silin.
  2. upload(), download(), delete(), list(), ensureDirectory() yazın. Hepsi void döner, hatada fırlatır.
  3. Sağlayıcınızda ucuz bir boyut çağrısı varsa remoteSize() geçersiz kılın; varsayılan onu list() üzerinden türetir.
  4. Uzun aktarımlara ilerleme geri çağrısını bağlayın.

4. OAuth Yarısını Ekleyin

  1. authorizationUrl(), exchangeCode(), refreshToken() ve revokeTokens() uygulayın.
  2. Geri çağrı rotası yazmayın. Ortak işleyici imzalı durumu doğrular, exchangeCode() çağırır ve sonucu geri gönderir.
  3. Jeton alanlarını configuration() içinde gizli kayıt olarak bildirin: erişim jetonu, yenileme jetonu, bitiş zamanı, hesap.
  4. Tembel yenileyin: her çağrıdan önce bitiş zamanına bakın.

Referans

StorageModule

imzalar
// 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;
remoteSize() null döndürmesi "Sağlayıcı yanıtlayamadı" demektir, başarısızlık değil. Yedek doğrulanmamış saklanır; sıfır doğrulamayı düşürür.
getPresignedDownloadUrl() Kısa ömürlü doğrudan adres; indirme oraya yönlenir. Karşılığı yoksa null kalır.
downloadStream() İstemciniz akış yapabiliyorsa geçersiz kılın; aksi hâlde arşiv geçici dosyaya yazılır.
controller_test() Zaten yazılmış: test() çağırıp formun beklediği biçimi döndürür.

CloudStorageModule

imzalar
// $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:

ortak geri çağrı
// 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.

AnahtarKabul ettiğiNe yapar
typetext, number, password, checkbox, select, hidden, section, redirect_uri, oauth_connectKontrolü seçer. Son üçü yerleşimdir: başlık, adres, oturum butonu.
namedizeConfig anahtarı; $this->config['name'] olarak okunur.
labeldizeGörünen etiket; $this->lang üzerinden düz bir yedekle okuyun.
width1 ile 12 arasıIzgara sütunu.
requiredboolAlanı zorunlu işaretler.
encryptedboolArayüz için sır işaretidir. Hiçbir şeyi şifrelemez: şifreleme encryptedFields() ile olur, ikisi uyuşmalıdır.
value, checked, placeholder, descriptionkarışıkBaşlangıç değeri, işaret durumu, ipucu, yardım metni.
step, doc_url, doc_labelint, dize, dizesection kaydında: numarası ve konsol bağlantısı.

Kimlik Bilgileri Nerede Durur

backup_storage satırı Hedef başına bir satır: ad, type, şifreli JSON config.
yapıcıdaki birleştirme meta ve defaults okur, sonra geçirilen config'i üzerine $this->config içinde birleştirir.
fabrikanın üçüncü argümanı Modules::getInstance("Storage", $type, [$config]). Sarmalama yapıcının argüman listesidir.
maske Şifreli alanlar beş yıldız olarak geri okunur; o değeri göndermek "değişmedi" demektir.
kayıtta yeniden doğrulama Güncelleme 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.

helpers/Backup.php
$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.

AcmeVault.php
<?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;
    }
}
config.php
<?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' => '',
    ],
];
çekirdek tarafı, kopyalamayın
// 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

encryptedFields() dışındaki sır okunabilir kalır

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.

Ölçemiyorsanız asla sıfır bildirmeyin

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.

Sessiz yükleme bayat sayılır

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.

Fabrika config'i bir seviye sarmalı alır

Modules::getInstance("Storage", $type, [$config]) üçüncü argümanı yapıcının argüman listesidir; sarmalanmamış config değerlere dağılır.

Bağlantı kesilirken jetonu iptal edin

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.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.