Всем доброго времени суток!
Хочу рассказать о том как можно, используя FUSE написать программу-клиент для Яндекс.Диск и подобных сервисов. У программы будет несложный, но симпатишный GUI.
Что нам понадобиться
При приготовлении программы будем использоваться следующие ингредиенты:
- С++
- Qt (4.x)
- curl
- libxml
- FUSE (Dokan для Windows)
- Яндекс.Диск API
Все находится в открытом доступе. Думаю у вас не возникнет проблем все это найти и скачать.
По задумке, программа должна быть легкой в использовании. Должна работать как на Linux, так и на Windows. Плюс хочется, чтобы можно было относительно несложно расширять функционал программы, посредством подключения других сервисов. Как то: ВКонтакте, Google.Docs и т.п.
Архитектура
Программа будет состоять из следующих крупных блоков:
- UI
- FUSE или Dokan (выбрать по вкусу)
- Local Driver
- Remote Driver
- Connector
- И часть которая, все это будет соединять, назовем ее Common
Диаграмма с основными блоками и их составными частями будет выглядеть следующим образом:
UI
Тут я думаю ничего интересного. Обычный Qt. Диалог с настройками выглядит таким образом:
FUSE
Используя драйвер файловой системы мы сможем отслеживать все операции над нужными нам файлами и уведомлять сторонние сервисы обо всех изменениях. Например, мы можем сохранить фотографию на нашем виртуальном диске. Потом открыть ее, например, в Gimp и отредактировать. Далее сохранить изменения прямо в Gimp и эти изменения автоматически попадут в облако Яндекс.Диска. FUSE уведомит нас, что определенная фотография изменилась и мы сможем отправить эти изменения в облако. На самом деле отслеживать изменения файлов можно и другими способами, но вариант с FUSE показался самым интересным. Хотя стоит признать, что он и очень сложный. Например, в той же Windows легко получить синий экран смерти, если некорректно обработать вызов Dokan'а.
Local Driver
Это фактически обертка для драйвера файловой системы в пользовательском пространстве. Напомню, в нашем случае этими драйверами являются FUSE или Dokan, в зависимости от операционной системы. В задачу этой самой обертки входит реакция на вызовы драйвера файловой системы и проброска их уже в Common часть. Обертка нам нужна для того, чтобы можно было подменять бэкенд в лице FUSE на Dokan и обратно, и при этом ничего не менять в остальной части программы. Для Fuse нам нужно обработать следующие вызовы драйвера файловой системы:
static int fuseGetAttr(const char *path, struct stat *statbuf);
static int fuseReadLink(const char *path, char *link, size_t size);
static int fuseMknod(const char *path, mode_t mode, dev_t dev);
static int fuseMkdir(const char *path, mode_t mode);
static int fuseUnlink(const char *path);
static int fuseRmdir(const char *path);
static int fuseSymlink(const char *path, const char *link);
static int fuseRename(const char *path, const char *newpath);
static int fuseLink(const char *path, const char *newpath);
static int fuseChmod(const char *path, mode_t mode);
static int fuseChown(const char *path, uid_t uid, gid_t gid);
static int fuseTruncate(const char *path, off_t newSize);
static int fuseUtime(const char *path, struct utimbuf *ubuf);
static int fuseOpen(const char *path, struct fuse_file_info *fileInfo);
static int fuseRead(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fileInfo);
static int fuseWrite(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fileInfo);
static int fuseStatfs(const char *path, struct statvfs *statInfo);
static int fuseFlush(const char *path, struct fuse_file_info *fileInfo);
static int fuseRelease(const char *path, struct fuse_file_info *fileInfo);
static int fuseFsync(const char *path, int datasync, struct fuse_file_info *fi);
static int fuseSetxAttr(const char *path, const char *name, const char *value, size_t size, int flags);
static int fuseGetxAttr(const char *path, const char *name, char *value, size_t size);
static int fuseListxAttr(const char *path, char *list, size_t size);
static int fuseRemovexAttr(const char *path, const char *name);
static int fuseOpenDir(const char *path, struct fuse_file_info *fileInfo);
static int fuseReadDir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fileInfo);
static int fuseReleaseDir(const char *path, struct fuse_file_info *fileInfo);
static int fuseFsyncDir(const char *path, int datasync, struct fuse_file_info *fileInfo);
static void* fuseInit(struct fuse_conn_info *conn);
static int fuseUtimens(const char *path, const struct timespec ts[2]);
Реализацию я приводить здесь не буду. Ее можно посмотреть в репозитории проекта, в файле linux_lvfs_driver.cpp
Аналог для Windows лежит здесь
Remote Driver
Тут надо сделать небольшое отступление. Как я уже писал выше, одним из требований к программе было возможность подключать различные сторонние сервисы. Для этого будем использовать систему плагинов, реализованную с использованием Qt. Тут тоже не будет каких-то откровений, при желании про плагины найдете много инфы в интернетах.
Дык вот Remote Driver — это абстракция для управления неким абстрактным плагином. Через Remote Driver будет происходить общение Common части нашей программы с конкретным плагином, реализующим работу со сторонним сервисом.
Connector
Connector является еще одной очень важной частью нашей программы. В задачу Connector'a входит абстрагировать работу c API различных сервисов. Будь то API Яндекс.Диск, Яндекс.Фоток или ВКонтакте. Приведу здесь объявление класса коннектора для Яндекс.Диск:
class YaDiskHTTPConnector : public QObject
{
Q_OBJECT
public:
YaDiskHTTPConnector();
~YaDiskHTTPConnector();
void setSettings(const QString& login
, const QString& password
, const QString& proxy
, const QString& proxyLoginPwd
, bool isOAuth
, const QString& token);
RESULT getTreeElements(const QString& path, QString& response);
RESULT downloadFile(const QString& url, const QString& path);
RESULT downloadFiles(const QList <QString>& urlList, const QList <QString>& pathList);
RESULT uploadFile(const QString& path, const QString& title, const QString& parentId, QString& response);
RESULT deleteFile(const QString& path, QString& response);
RESULT createDirectory(const QString& title, const QString& parentId, QString& response);
RESULT moveElement(const QString& id, const QString& oldParentId, const QString& newParentId, ElementType type, QString& response);
RESULT renameElement(const QString& id, ElementType type, const QString& newTitle, QString& response);
void setToken(const QString& token);
private:
static size_t writeStr(void *ptr, size_t size, size_t count, void *response);
static size_t fwrite_b(void *ptr, size_t size, size_t count, void *path);
static size_t readStr(void *ptr, size_t size, size_t nmemb, void *stream);
static size_t read_callback(void *ptr, size_t size, size_t nmemb, void *stream);
int execQuery(const QString &url, const QString &header, const QString &postFields, QString* response);
private:
struct sPutData
{
const char* m_data;
size_t m_len;
};
private:
QString m_login;
QString m_password;
QString m_proxy;
QString m_proxyLoginPwd;
bool m_isOAuth;
QString m_token;
QString m_requestId;
QString m_key;
QMutex m_connectorMutex;
};
На самом деле похожий вид имеет любой другой коннектор к стороннему сервису. На данный момент реализованы коннекторы для следующих сервисов:
- Яндекс.Диск
- Яндекс.Фотки
- Facebook (работа с фото)
- Вконтакте(работа с фото)
- Google.Docs
Реализацию коннектора для Яндекс.Диск можно найти в этом файле. Для отсылки запросов к сервису используется всем известный CURL.
Ну вот и все
Объединив все это вместе, получим наш простенький клиент, работающий под двумя операционными системами и с различными облачными сервисами.
Полную версию исходников проекта можно найти ТУТ.
Если вы хотите помочь в развитии программы — добро пожаловать!
P.S. Ссылка на инсталлятор для Windows. Под Linux скомпилировать сейчас нет возможности. Чуть позже выложу.
Автор: ershovdz