Portál AbcLinuxu, 20. května 2024 09:43

KDE pro programátory – KIO a jeho otroci

3. 3. 2011 | Dan Vrátil
Články - KDE pro programátory – KIO a jeho otroci  

KIO je technologie, která v KDE zahrnuje téměř všechno kolem vstupně-výstupních operací a poskytuje abstrakci pro přístup ke vzdáleným úložištím. Samotnou komunikaci s úložišti pak zajišťují procesy zvané KIO Slaves.

Obsah

KIO je technologie, která zajišťuje téměř vše okolo vstupně-výstupních operací v KDE. V rámcí API sdružuje vše, co se týká práce se soubory, či adresáři. Jmenný prostor KIO ale zapouzdřuje i třídy pro práci s SSL, záložkami, různé dialogy a další.

Nad lokálním souborovým systémem umí vytvořit další virtuální vrstvu, do které se připojí i vzdálená uložiště tak, že je můžete procházet jako lokální. Nemusí se samozřejmě vždy jednat o vzdálené úložiště, díky KIO můžete procházet i archivy, manuálové stránky atd. Samotný přístup ke uložištím pak zajišťují speciální procesy, tzv. KIO Slaves.

KIO Jobs

link

KIO Jobs jsou nástroje určené pro kopírování, přesouvání a mazání souborů či adresářů a dokáží vcelku usnadnit práci se soubory. Nemusíte se starat vůbec o nic, stačí předat KUrl zdrojového a cílového souboru či adresáře, připojit se na signály a pak už sledovat průběh operace.

#include <KIO/CopyJob>
#include <KUrl>
...
...

KUrl source("/home/dan/Music");
KUrl destination("/mnt/backups");
KUrl link("/home/dan/Hudba");

// Zkopiruje slozku /home/dan/Music do /mnt/backups
KIO::CopyJob *copyJob = KIO::copy(source, destination, HideProgressInfo | Overwrite);
// Presune slozku /home/dan/Music do /mnt/backups
KIO::CopyJob *moveJob = KIO::move(source, destination, HideProgressInfo | Overwrite);
// Vytvori symlink z /home/dan/Music do /home/dan/Hudba
KIO::CopyJob *linkJob = KIO::link(source, link, HideProgressInfo);

Příznak HideProgressInfo skryje grafické zobrazení průběhu (tzn. že kopírování se neobjeví v notifikacích v systémové části panelu). S příznakem Overwrite kopírování neselže, pokud adresář nebo soubor existují.

KIO::CopyJob pak disponuje mnoha sloty a signály, přes které je možné ovlivňovat a sledovat průběh. Pro sledování jsou zajímavé signály totalFiles() a totalDirs(), které jsou vyvolány, když proces spočítá, kolik souborů a adresářů se bude zpracovávat. Signály processedFiles() a processedDirs() průběžně informují o počtu zpracovaných souborů, respektive adresářů. processedSize() informuje o velikosti přenesených dat v bajtech. Pokud chcete zobrazovat, jak se jmenuje aktuálně kopírovaný soubor, můžete poslouchat signál copying(). Signál je vyvolán každých 200ms, aby nedocházelo ke zbytečnému zpomalování procesu. Existují také analogické signály moving() a linking(). U KIO::link() se vytvoří buď klasický unixový symlink, nebo, pokud source a destination používají jiné protokoly nebo jsou vzdálené, se vytvoří .desktop odkaz. Když je celý Job hotový, emituje se signál result().

Pomocí metod doSuspend(), doResume() a doKill() můžete proces pozastavit, znovu spustit a nebo úplně zrušit. U prvních dvou metod je vyvolán signál suspended(), respektive resumed().

Pro ukázku v praxi vytvoříme jednoduchý program pro kopírování a přesouvání adresářů.

...
...
void MainWindow::copyDir()
{
    KFileItem src_item = src_model->itemForIndex(src_treeView->currentIndex());
    KFileItem dest_item = dest_model->itemForIndex(dest_treeView->currentIndex());

    ...
    ...
  
    if (sender() == copy_btn)
      current_job = KIO::copy(src_item.url(), dest_item.url(), KIO::HideProgressInfo);
    else
      current_job = KIO::move(src_item.url(), dest_item.url(), KIO::HideProgressInfo);

    // Informuje o celkovem poctu souboru, adresaru a bajtu, ktere se budou prenaset
    connect(current_job, SIGNAL(totalAmount(KJob*, KJob::Unit, qulonglong)),
	    this, SLOT(setTotalAmount(KJob*, KJob::Unit, qulonglong)));

    // Informuje o jiz prenesenem mnozstvi souboru, adresaru a bajtu
    connect(current_job, SIGNAL(processedAmount(KJob*, KJob::Unit, qulonglong)),
	    this, SLOT(setProcessedAmount(KJob*, KJob::Unit, qulonglong)));

    // Informuje o prave prenasenem souboru
    connect(current_job, SIGNAL(copying(KIO::Job*, KUrl, KUrl)),
	    this, SLOT(setCurrentFile(KIO::Job*, KUrl, KUrl)));

    // Informuje o procentualnim progresu
    connect(current_job, SIGNAL(percent(KJob*, ulong)),
	    this, SLOT(setPercent(KJob*, ulong)));

    // Informuje, ze prenos byl dokoncen
    connect(current_job, SIGNAL(finished(KJob*)),
	    this, SLOT(jobFinished(KJob*)));

    // Informuje, ze prenos byl pozastaven
    connect(current_job, SIGNAL(suspended(KJob*)),
	    this, SLOT(setProcessSuspended(KJob*)));

    // Informuje, ze prenos byl znovu spusten
    connect(current_job, SIGNAL(resumed(KJob*)),
	    this, SLOT(setProcessResumed(KJob*)));

    // Informuje o aktualni rychlosti prenosu (v bajtech za sekundu)
    connect(current_job, SIGNAL(speed(KJob*, ulong)),
	    this, SLOT(setSpeed(KJob*, ulong)));

    // Pri kliknuti na suspend_btn pozatavi prenos
    connect(suspend_btn, SIGNAL(clicked(bool)),
	    current_job, SLOT(suspend()));

    // Pri kliknuti na kill_btn zrusi prenos
    connect(kill_btn, SIGNAL(clicked(bool)),
	    current_job, SLOT(kill()));
    
    processed_dirs = 0;
    processed_files = 0;
    ...
    ...
    // Zobrazi dalsi prvky UI
    progress_bar->setVisible(true);
    progress_label->setVisible(true);
    suspend_btn->setVisible(true);
    kill_btn->setVisible(true);
    disableUI(true);
    
    // Nastavi titulek
    status = "Running";
}
...
...

V copyDir() vytvoříme nový KIO::CopyJob a napojíme se na některého jeho signály. Protože nás zajímá počet celkových a zpracovaných souborů, adresářů i bajtů (tedy všechno), použijeme místo signálů a slotů pro jednotlivé hodnoty trochu univerzálnější přístup: totalAmount() a processedAmount(). Druhým parametrem těchto signálů je výčtový typ KJob::Unit, který určuje, jaká hodnota se předává ve třetím parametru.

...
...
void MainWindow::setTotalAmount(KJob* job, KJob::Unit unit, qulonglong amount)
{
    switch (unit)
    {
      case KJob::Bytes:
	total_size = amount;
	break;
	
      case KJob::Files:
	total_files = amount;
	break;
	
      case KJob::Directories:
	total_dirs = amount;
	break;
    }

    updateUI();

    Q_UNUSED(job)
}

void MainWindow::setProcessedAmount(KJob* job, KJob::Unit unit, qulonglong amount)
{
    switch (unit)
    {
      case KJob::Bytes:
	processed_size = amount;
	break;
	
      case KJob::Files:
	processed_files = amount;
	break;
	
      case KJob::Directories:
	processed_dirs = amount;
	break;
    }

    updateUI();
    
    Q_UNUSED(job)
}
...
...

Obdobně můžete sbírat i další informace jako rychlost přenosu, aktuálně přenášený soubor nebo průběh přenosu v procentech:

...
...
void MainWindow::setCurrentFile(KIO::Job* job, KUrl src, KUrl dest)
{
    current_src = src;
    current_dest = dest;
    updateUI();
    
    Q_UNUSED(job)
}

void MainWindow::setSpeed(KJob* job, ulong speed)
{
    current_speed = speed;
    updateUI();
    
    Q_UNUSED(job)
}

void MainWindow::setPercent(KJob* job, ulong percentage)
{
    current_percentage = percentage;
    updateUI();
    
    Q_UNUSED(job);
}
...
...
void MainWindow::updateUI()
{

    progress_bar->setValue(current_percentage);
    
    QString str;
    
    str = "

%1


Files: %2 of %3
Dirs: %4 of %5
Size: %6 of %7 MiB
Speed: %8 MiB/s
%9 -> %10"; str = str.arg(status, QString::number(processed_files), QString::number(total_files), QString::number(processed_dirs), QString::number(total_dirs), QString::number(processed_size/1048576), QString::number(total_size/1048576), QString::number(current_speed/1048576)).arg( current_src.path(), current_dest.path()); progress_label->setText(str); }

Metodou updateUI() už jen aktualizujeme uživatelské rozhraní.

Pokud proces během kopírování nebo přesunu narazí na existující složku nebo adresář, zeptá se klasickým KDE dialogem na další akci (přepsat, ignorovat, ...), stejně tak pokud se mu nepodaří přečíst nebo zapsat nějaký soubor. Všechny dialogy a okna jsou řešeny automaticky. Toto chování lze potlačít nastavením vlastností pomocí metod setAutoSkip(), setDefaultPermissions(), setWriteIntoExistingDirectories() nebo příznakem Overwrite v konstruktoru KIO::Job.

Mimo KIO::CopyJob existuje ještě například KIO::PreviewJob, který pro soubor zadaný v konstruktoru vygeneruje náhled se o zadaných rozměrech a vrátí ho jako QPixmap signálem gotPreview(), KIO::DeleteJob pro odstraňovaní souborů či adresářů, KIO::listDir() pro získání obsahu adresáře (případně KIO::listRecursive() pro rekurzivní výpis), nebo KIO::mimetype() pro získání MIME typu.

KDE pro programátory

KIO Slaves

link

Možná jste si všimli, že pokud procházíte nějaké vzdálené úložiště, spouští se na pozadí automaticky procesy s názvy jako kio_ftp, kio_http a další. Těmto procesům se říká KIO Slaves a právě ony zajišťují komunikaci se serverem a zkrze své API zpřístupňují informace o obsahu adresářů koncovým aplikacím. Nejde vlastně o samostatné spustitelné soubory, ale o pluginy aplikace kdeinit4.

Pomocí KIO Slaves lze zobrazovat cokoliv, co lze alespoň trochu zorganizovat do stromové struktury (adresáře, soubory). Pro ukázku toho, jak to může vypadat, jsem připravil KIO Slave, který umí procházet složku Software zde na AbcLinuxu.

Každý KIO Slave musí musí mít funkci kdemain() a samotné třídy otroka, odvozené od KIO::SlaveBase nebo KIO::TCPSlave.

abcslave.h

link
#include <KIO/SlaveBase>
#include <kio/udsentry.h>

#include <QByteArray>
#include <QtWebKit/QWebElement>

class ABCSlave:public QObject, public KIO::SlaveBase
{
  Q_OBJECT

  public:
    ABCSlave (const QByteArray & pool, const QByteArray & app);

    void stat (const KUrl & url);
    void listDir (const KUrl & url);
  
  private:
    QWebElement getWebPage(QString path);
  
    KIO::UDSEntry dirEntry(QWebElement item);
    KIO::UDSEntry articleEntry(QWebElement item);
};

V této ukázkce jsem reimplementoval pouze metody stat() a listDir(). První zmíněná metoda slouží ke zjišťování informací o zadané URL (jedná se o soubor, nebo adresář, jaká jsou přístupová práva, MIME typ atd.). V druhé metodě se pak generuje seznam souborů či adresářů na uvedené URL. Dále existují metody copy(), move(), rename(), del(), chmod(), chown(), mkdir(), symlink(), read() a write(). Tyto metody můžete, ale nemusíte reimplementovat, záleží na tom, co všechno protokol umí, nebo jaké možnosti chcete poskytovat. Nám stačí jen procházení adresářů. Jedná se vlastně o samotné implementace stejnojmenných volání z KIO::Job.

Informace o adresáři nebo souboru se předávají pomocí objektů KIO::UDSEntry(), jak uvidíte v kódu níže. V ukázce nastavuji jen několik základních nejdůležitějších informací, no ve skutečnosti jich je mnohem více.

abcslave.cpp

link
...
...
// Vyexportuje funkce kdemain()
extern "C"
{
  KDE_EXPORT int kdemain (int argc, char **argv);
}

// Hlavni funkce knihovny
int kdemain (int argc, char **argv)
{
  // Vytvori novou aplikaci
  QApplication app(argc, argv);
  
  // Nahlasi se KDE jako nova instance kio_abc
  KComponentData instance ("kio_abc");

  // Vytvori novy ABCSlave
  ABCSlave *slave = new ABCSlave (argv[2], argv[3]);
  slave->dispatchLoop ();
  
  delete slave;

  return 0;
}

Důležité je funkci kdemain() nadeklarovat jako "extern". Tato funkce je ekvivalentem klasické main(). Důležité je nezapomenout spustit hlavní smyčku (KIO::SlaveBase::dispatchLoop()), a v případě, že potřebujete používat signály a sloty, tak je potřeba vytvořit objekt QApplication().

void ABCSlave::listDir(const KUrl& url)
{
    // Nacte webovou stranku 
    QWebElement webelement = getWebPage(url.path());
    
    // Najde vsechny &al;UL>
    QWebElementCollection uls = webelement.findAll("ul");
    QWebElement treemenu;
    
    // Prohleda vsechny <UL>, skonci, kdyz najde <UL id="treemenu1">
    for (int i = 0; i < uls.count(); i++)
    {
	if (uls.at(i).attribute("id", "") == "treemenu1") {
	  treemenu = uls.at(i);
	  break;
	}  
    }
   
    // Projde vsechny potomky v nalezenem <UL>
    QWebElement item = treemenu.firstChild();
    listEntry(dirEntry(item), false);
    
    while (!item.nextSibling().isNull())
    {
	item = item.nextSibling();
	// Kazdeho potomka prida do listEntry() jako adresare
	listEntry(dirEntry(item), false);
    }
    
    // Najde vsechny tabulky
    QWebElementCollection tables = webelement.findAll("table");
    QWebElement sw_table;
    // Projde vsechny tabulky, hleda <table class="sw-polozky">
    for (int i = 0; i < tables.count(); i++)
    {
	if (tables.at(i).attribute("class", "") == "sw-polozky") {
	  sw_table = tables.at(i);
	  break;
	}
    }
    
    /* V nalezene tabulce najde vsechny odkazy a vlozi je do listEntry()
       jako soubory */
    QWebElementCollection links = sw_table.findAll("a");
    for (int i = 0; i < links.count(); i++)
    {
	listEntry(articleEntry(links.at(i)), false);
    }

    // Odesle celou frontu
    listEntry(KIO::UDSEntry(), true);
    
    // Hotovo!
    finished();    
}

Metoda listDir() generuje obsah adresáře. V našem případě načte webovou stránku a potom prochází zdrojový kód a hledá určité speicifické elementy. Každý objevený prvek se do seznamu přidává metodou listEntry(), která se předá objekt KIO::UDSEntry. Druhý parametr metody indikuje, jestli už jsou všechny záznamy připraveny. Doporučuji nejdřív naakumulovat všechny záznamy a až potom zavolat listEntry() s prázdným KIO::UDSEntry() v prvním a hodnotou TRUE v druhém parametru. Při TRUE se všechna data odešlou klientovi k vizualizaci. Důležité je informovat signálem finished(), že procházení adresáře bylo dokončeno a už nic víc měnit nebudeme.

V případě selhání emitujeme signál error(), kde prvním argumentem je identifkátor chyby a druhým textový popis. Textový popis může být prázný (defaultní chyby mají vlastní popis), ale u kódů jako ERR_UNKNOWN nebo ERR_INTERNAL je vhodné krátce informovat o tom, co se pokazilo.

void ABCSlave::stat(const KUrl &url)
{
    KIO::UDSEntry entry;
    
    // O korenovem adresari vime, ze je to adresar, a ze do nej maji pristup vsichni
    if ((url == "") || (url == "/")) {
      entry.insert(KIO::UDSEntry::UDS_NAME, ".");
      entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
      entry.insert(KIO::UDSEntry::UDS_USER, "root");
      entry.insert(KIO::UDSEntry::UDS_GROUP, "root");
      entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
      statEntry(entry);
      finished();
      return;
    }

    // Pokud se nejedna o korenovy adresar, tak nacte stranku na dane adrese
    QWebElement web = getWebPage(url.path());
    QWebElementCollection uls = web.findAll("ul");
    
    // Pokud v ni najde <UL id="treeemenu1">, tak se jedna o adresar
    QWebElement treemenu;
    for (int i = 0; i < uls.count(); i++)
    {
	if (uls.at(i).attribute("id", "") == "treemenu1") {
	  entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
	  entry.insert(KIO::UDSEntry::UDS_NAME, url.directory());
	  entry.insert(KIO::UDSEntry::UDS_URL, url.path());
	  statEntry(entry);
	  finished();
	  return;
	}  
    }
   
    // Pokud je na dane strance <div class="sw"> jedna se o stranku
    // konkretniho programu, tedy je to soubor
    QWebElementCollection divs = web.findAll("div");
    for (int i = 0; i < divs.count(); i++)
    {
	if (divs.at(i).attribute("class", "") == "sw") {
	  entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
	  entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, "text/html");
	  entry.insert(KIO::UDSEntry::UDS_NAME, divs.at(i).findFirst("h1").toPlainText());
	  entry.insert(KIO::UDSEntry::UDS_URL, "http://www.abclinuxu.cz/"+url.url());
	  statEntry(entry);
	  finished();
	  return;
	}
    }
    
    // No a pokud nenajdeme ani slozku ani clanek, tak jsme asi neco udelali spatne
    error(KIO::ERR_DOES_NOT_EXIST, "");
}

Naše metoda stat() obsluhuje tři případy: když zadaná URL ukazuje na kořenový adresář, když obsahuje podsložky nebo soubory, či když je to už samotný článek. Pomocí této metody si klientská aplikace může zjišťovat detailnější informace o obsahu libovolného adresáře či souboru. Standardní chování je, že si klient nejprve zjistí informace o adresáři, do kterého právě vstupuje a až po jeho načtení postupně načítá informace o jednotlivých podadresářích nebo souborech.

Metodou KIO::UDSEntry::insert() se do záznamu přidávaji další a další informace. Já v této ukázce vkládám jen ty nejdůležitější: typ (soubor vs. adresář), název, cestu, u souborů pak ještě MIME typ. Po vložení informace opět nesmíme zapomenout na sigál finished(). Pokud se funkce dostane až na konec, emituje se chyba.

KIO::UDSEntry ABCSlave::dirEntry(QWebElement item)
{
     // V predanem HTML radku najde odkaz
     QWebElement link = item.findFirst("a");
     
     KIO::UDSEntry entry;
     // Precteme text z <a> </a>
     entry.insert(KIO::UDSEntry::UDS_NAME, link.toPlainText());
     // Typ: slozka
     entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
     QString url = link.attribute("href", "");
     if (url.left(10) == "/software/") 
       url.remove(0,10);
     // Adresa, na kterou slozka odkazuje
     entry.insert(KIO::UDSEntry::UDS_URL, "abc:/"+url);
     
     return entry;
}

KIO::UDSEntry ABCSlave::articleEntry(QWebElement item)
{
     KIO::UDSEntry entry;
     // Nazev clanku
     entry.insert(KIO::UDSEntry::UDS_NAME, item.toPlainText());
     // Typ: regularni soubor
     entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
     QString url = item.attribute("href", "");
     if (url.left(10) == "/software/")
       url.remove(0, 10);
     // Adresa, na ktere se clanek nachazi
     entry.insert(KIO::UDSEntry::UDS_URL, "http://www.abclinuxu.cz/software/"+url);
     // MIME Type 
     entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, "text/html");
     
     return entry;
}

Další ukázka vytváření vlastních KIO::UDSEntry.

Poslední částí KIO Slave je soubor .protocol, který obsahuje informace o schopnostech našeho otroka. Soubor může vypadat například takto:

[Protocol]
exec=kio_abc
protocol=abc
input=stream
output=filesystem
listing=Name,Type
deleting=false
linking=false
reading=true
writing=false
moving=false
source=false

exec je název knihovny bez přípony. protocol je název protokolu. Když do Dolphina či Konqueroru napíšete abc:/, KDE automaticky použijí našeho otroka. listing říká, jaké všechny informace v základě o položkách dodáváme. Následující direktivy informují o schopnostech otroka - ten náš umí pouze číst. Soubor .protocol se instaluje do /usr/share/kde4/services.

Když projekt přeložíte a nainstalujete, můžete ho otestovat vepsáním abc:/ do adresního řádku libovolného KDE správce souborů. Po chvíli načítání by se měl objevit seznam adresářů identický s tím v sekci Software.

Pokud se chcete podívat trochu hlouběji do KIO Slaves, podívejte se třeba do zdrojových kódu kio_ftp (kdelibs/kioslave/ftp), nebo kio_archive (kdebase-runtime/kioslave/archive).

KDE pro programátory

Závěr

link

KIO je prostředek pro snadnou práci se vstupně-výstupními operacemi a umožňuje pohodlný přístup i ke vzdáleným úložištím. Samotný přístup je možný díky procesům zvaným KIO Slaves. V KDE je ve výchozí instalaci k dispozici na 90 různých KIO Slaves, které podporují všelijaké síťové protokoly i lokální soubory.

V příští části se dostaneme k mnohými zatracované technologii Akonadi. Úplně nakonec ještě archiv se zdrojovými kódy příkladů.

Seriál KDE pro programátory (dílů: 3)

První díl: KDE pro programátory – úvod, poslední díl: KDE pro programátory – KIO a jeho otroci.
Předchozí díl: KDE pro programátory – KParts

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.