Журнал LinuxFormat - перейти на главную

LXF125:Python

Материал из Linuxformat
Перейти к: навигация, поиск
Python Заставим Web монтировать Google Docs в виде диска

Содержание

Python: Сделаем Google диском

Конвертирование документов по запросу при помощи монстра а-ля доктор Моро – гибрида Google Docs, Ника Вейча, Python и Fuse.

Поработайте с Unix-системами подольше, и все для вас станет файлом. Ваш список задач? Это файл. Дата? Тоже файл. Ваша мышь? И она – файл. Пластиковый пакет, куда вы вкладываете бумаги – ну, вы в курсе.

Да, абстрактное восприятие всего в виде файлов имеет смысл в среде со множеством инструментов для использования файлов и работы с ними. Их можно копировать, читать, выводить список и перемещать при помощи нескольких команд. А каков будет мир, если мы сделаем файлом Интернет? Давайте посмотрим...

Применим Fuse

Fuse, или Filesystem in Userspace [файловая система пространства пользователя] – это проект, стартовавший в 2004 г. Его цель – предоставить способ монтирования файловых систем на уровне пользователя и сделать возможным написание программных виртуальных файловых систем. Работает он прекрасно, и, что замечательно, для создания Fuse-приложений нужен всего лишь Fuse-модуль ядра и библиотеки для любимого языка программирования. На данном уроке мы приложим руки к модулю Python под названием python-fuse.

Создать виртуальную файловую систему не просто, но и не так сложно, как думают многие. Большую часть работы выполняет Fuse, а вам остается только предоставить код, который решает, что содержит файловая система, и возвращает информацию стандартным вызовам ОС для управления файлами. Вам не стоит беспокоиться о способах выполнения копирования – нужны только низкоуровневые вещи: имя файла, что в нем находится и какие атрибуты у него в файловой системе.

Простая реализация Fuse не требует много кода, если ваш класс – потомок класса fuse Python: все что нужно сделать – это определить несколько методов, зависящих от того, какие возможности вы хотите реализовать в файловой системе. В простейшем случае реализуйте следующее:

  • getattr возвращает такие значения, как размер файла и права.
  • readdir возвращает список содержимого каталога.
  • open возвращает дескриптор открытого файла.
  • read возвращает содержимое файла.

Для файловой системы с возможностью записи следует описать дополнительные методы. Вот как может выглядеть простая реализация:



 class MyStat(fuse.Stat):
   def __init__(self):
     self.st_mode = 0
     self.st_ino = 0
     self.st_dev = 0
     self.st_nlink = 0
     self.st_uid = 0
     self.st_gid = 0
     self.st_size = 0
     self.st_atime = 0
     self.st_mtime = 0
     self.st_ctime = 0
 class MyFS(Fuse):
   def getattr(self, path):
     st = MyStat()
     st.st_atime = int(time.time())
     st.st_mtime = st.st_atime
     st.st_ctime = st.st_atime
     st.st_mode = stat.S_IFDIR | 0755
     st.st_nlink = 2
     st.st_size=4096
     return st
   def readdir(self, path, offset):
     dirents =[‘.’, ‘..’]
     if path == ‘/’:
        dirents.extend([‘foo’, ‘bar’])
     for e in dirents:
        yield fuse.Direntry(e)
   def open(self, path, flags):
     filehandle=open(ваш_файл,”r”)
     return filehandle
  def read(self, path, size, offset=0, filehandle=None):
     filehandle.seek(offset)
     buffer= filehandle.read(size)
     return buffer

Эта (не рабочая) реализация показывает грубую структуру приложения, которое мы хотим создать. Стоит отметить несколько важных пунктов: каждый каталог должен содержать стандартные для Unix записи ‘.’ и ‘..’, и их всегда надо возвращать. Метод open может возвращать объект – обычно дескриптор файла, но мы сами вольны решать, чем он будет и как его использовать. Позже такой объект будет передан на вход метода read, что удобно, так как мы должны откуда-то брать данные, возвращаемые в ответ на его вызов (т.е. выполнять операции чтения).

На данный момент у нас упущена обработка ошибок. Она важнее, чем может показаться, потому что к любой смонтированной файловой системе современного дистрибутива обращается много стандартных служб ОС, выполняющих проверку наличия файла автозапуска, индексирование документов или очистку корзины. В финальной версии кода необходимо возвращать ошибки типа Файл не найден, для поддержания системы в рабочем состоянии.

Итак, мы собираемся расширить имеющееся, заставив файловую систему читать Google Docs. Наша цель – возможность монтирования файловой системы, отображающей наши документы и позволяющей читать их, словно они находятся на локальном диске.

Перенаправление stderr

Запустив кусок кода вроде файловой системы Fuse, вы заметите, что он выполняется в собственном потоке. Это прекрасно, но означает, что вы не сможете получить из Python сообщения stderr, необходимые для отладки. Как известно, благодаря способу обработки исключений в коде Fuse, он может генерировать до 50 исключений в секунду, и никто о них не узнает. Ну, разве что файловая система не будет работать. Вы можете создать файл журнала для захвата этих данных при помощи

 fsock=open(‘/home/evilnick/error.log’,“a”)
 fsock.writelines(**Started**)
 sys.stderr = fsock

Погружаемся в Docs

Для своего сервиса Docs, Google предоставляет API (LXF120); имеется также исчерпывающий набор официальных API-модулей для различных языков, включая Python. Он доступен во всех ведущих дистрибутивах, и вы можете установить python-gdata из вашего менеджера пакетов.

Сам пакет gdata содержит API для практически всех сервисов Google и часто обновляется. Мы настоятельно рекомендуем обновить ваш пакет: в противном случае вы можете обнаружить, что результаты выполнения кода могут слегка отличаться.

Модуль gdata – это однородная масса, но пространство имен разбито на части, связанные с различными службами, и вам нет нужды работать со всей библиотекой: достаточно малой толики. Давайте опробуем что-нибудь в интерактивной оболочке Python:

 >>> import gdata.docs.service
 >>> user = ‘<ваше_имя_поль зователм>@gmail.com>>> password = ‘<ваш_пароль>>>> client=gdata.docs.service.DocsService()
 >>> r = client.ClientLogin(user,password, source=”evil script”)
 >>> gdata.docs.service.SUPPORTED_FILETYPES
 {‘XLSX’: ‘application/vnd.openxmlf...
 ...
 ...’XLS’: ‘application/vnd.ms-excel’, ‘PNG’: ‘image/png’}

Как видите, мы просто импортировали кусок модуля и авторизовались в сервисе стандартным способом, путем создания объекта и вызова метода ClientLogin. Здесь указываются имя пользователя и пароль, а также строка источника, что упрощает для Google отслеживание действий клиента.

Google Docs поддерживает только документы в определенных форматах, что и понятно – это не файлообменник, и серверы Google должны иметь возможность понять, что к ним загружают. Вы можете найти текущий список путем вывода значения переменной модуля SUPPORTED_FILETYPES (в коде приведена не полностью), организованной в виде списка пар расширения файла и типов MIME.

Чтобы работать с сохраненными документам в Google Docs, необходимо знать, что они собой представляют. Объект client имеет метод для получения списка документов, возвращающий объект feed, содержащий пронумерованный список записей. Каждая запись имеет несколько свойств, включая имя, тип и уникальный ID-номер.

 >>> list=client.GetDocumentListFeed()
 >>> list
 <gdata.docs.DocumentListFeed object at 0xa066f2c>
 >>> for entry in list.entry:
 ... print (entry.title.text, entry.GetDocumentType, entry.resourceId.text)
 ...(“fun things we’ve done”, ‘document’,‘document:dgg88xxxxxx xfs’)
 (‘letters raw text’, ‘document’, ‘document:df4m88888888f9’)
 (‘cover’, ‘document’, ‘document:df4mf7888888888fk’)
 (“Pamela’s Cover Letter - June 2009”, ‘document’, ‘document:df4m88888888888gs’)
 (‘Untitled Presentation’, ‘presentation’, ‘presentation:dd26888888888d9’)
 (‘sharing and workflow’, ‘document’, ‘document:df4mf7c7_18888888888g6’)

Теперь мы можем воткнуть это в нашу конструкцию Fuse, чтобы получить список имеющихся документов. Мы можем пожелать загрузить их в различных форматах, поддерживаемых Google (DOC, ODT, PDF и так да лее), так что вы можете создать ката логи и представить файлы в них, словно они имеют соответствующий тип:

  def readdir(self, path, offset):
    dirents =[‘.’, ‘..’]
    if path == ‘/’:
       dirents.extend([‘odt’,’doc’,’pdf’])
    if path == ‘/odt’:
       # полу чаем доку мент с gmail
       client=gdata.docs.service.DocsService()
       client.ClientLogin(user, password, source=”evil script”)
       list=client.GetDocumentListFeed()
       for entry in list.entry:
          if entry.GetDocumentType() == ‘document’ :
             dirents.append(entry.title.text+’.odt)
       #вставьте код для дру гих ката логов
    for e in dirents:
       yield fuse.Direntry(e)

Здесь мы включили код в класс нашей файловой системы. Теперь у нас есть корневой каталог – все относительно точки монтирования – и мы хотим вывести список содержимого наших каталогов с документами. Для первого из них, каталога odt, мы подключаемся к Google и загружаем список доступных документов, отсекая все, что не является ODF-документом. Далее используем метод fuse для возвращения путей с именами.

Это было вполне очевидно, но как только ОС выведет список каталога, она попробует проверить атрибуты каждой записи, поэтому нам необходимо создать и этот код. Для каталогов будем считать, что времена создания и изменения не важны. Мы можем создать глобальную переменную и помечать все при создании файловой системы, но это не стоит трудов. При выводе списка в оболочке каталоги всегда имеют размер 4096, и мы можем просто так и записать. Что касается прав, мы воспользуемся переменными модуля stat, созданного в рассмотренном выше простейшем примере, для установки стандартной записи для каталога и придания ей основных прав доступа.

С файлом все малость сложнее. Мы вполне можем создать все значения для прав, времени и так далее, но как быть с размером? Реальных способов узнать размер до завершения загрузки нет, а поскольку его не существует, то это невозможно. К счастью, мы часто можем вернуть стандартный размер файла, что позволит избежать множества проблем (хотя и приведет к неверному результату выполнения команды du).

Выдача неверного размера файла при запросе содержимого каталога – сравнительно безобидная шалость, но передача неверного размера чему-то, что пытается прочесть и открыть его целиком, может открыть ящик Пандоры: создастся дыра в пространственно-временном континууме, погибнет галактика, или открытие файла завершится на полполовине. И вновь есть уловка, которая поможет обойти это ограничение.

Идем на хитрость

Для начала, вам необходимо знать, что когда файловая система читает файл – например, при операции копирования – это всегда выполняется вызовами open, getattr, read, именно в таком порядке. Значит, нам надо только загрузить файл при получении вызова open, а затем поместить его в кэш. Вызов getattr можно использовать для проверки, что файл уже в кэше, и определения его точного размера. Поскольку файл проверяется до того, как начнется чтение, будет получен верный размер, и вызывающее нас приложение даже не будет знать, что мы его одурачили.

Более того, настройка кэша выполняется просто, если импортировать tempfile, стандартный модуль Python. Среди множества его опций есть метод создания временного каталога, который обычно размещается в /tmp, но это может меняться в зависимости от настроек системы. Метод, приведенный ниже, возвращает путь, и вы можете просто добавить, что нужно, до и после него:

cachedir=tempfile.mkdtemp(prefix=’fuse-’)
os.mkdir(os.path.join(cachedir, ‘odt’))
os.mkdir(os.path.join(cachedir, ‘pdf’))

Мы также создали несколько каталогов для хранения наших файлов; обратите внимание, что мы хотим воссоздать структуру нашей виртуальной файловой системы, чтобы легко находить кэшированные объекты.

После настройки каталога кэша его необходимо заполнять по мере надобности. Загрузка всего при каждом выводе списка каталога будет перебором, но нам необходимо скачивать последнюю версию файла при каждом его открытии. Поэтому при вызове open мы загружаем файл с Google и помещаем его в кэш. Далее нам необходимо открыть файл и вернуть дескриптор файла для нужд того, что его открывает.


  def open(self, path, flags):
    path_element = path.split(‘/’)
    fname=path_element[-1]
    #полу чаем файл с Google
    client=gdata.docs.service.DocsService()
    client.ClientLogin(user,password, source=”evil script”)
    q = gdata.docs.service.DocumentQuery()
    q[‘title’] = fname[:-4]
    q[‘title-exact’] = ‘true’
    list = client.Query(q.ToUri())
    tfile=os.path.join(cachedir, path[1:])
    client.Export(list.entry[0], tfile)
    filehandle=open(tfile,”r”)
    return filehandle

В этом коде мы используем один метод для загрузки файла с Google. Все, что нам известно о файле – его имя, и мы можем сделать запрос на его основе и направить его в Google, чтобы получить список файлов с указанными именами. Запрос отсылается, и мы получаем первый элемент списка, но если у вас два файла с одинаковыми именами, то это может стать проблемой. Одним из решений будет чтение всех документов в класс и сохранение дескрипторов на каждый файл, но при наличии файлов с одинаковыми именами в списке содержимого каталога все равно будет путаница. Мы будем считать, что все файлы имеют разные имена.

Экспорт неопределенности

Метод Export модуля Google весьма любопытен. Он принимает только объект entry – вы получаете его из запроса – и имя файла. Однако документы подчиняются некому принципу неопределенности, поскольку вы не знаете, в каком они формате, пока не спросите. То, как с этим справляется API, очень занятно: вместо указания желаемого типа файла он угадывается по имени, который вы передаете методу клиента. Другим моментом является то, что вместо передачи вам данных, клиент желает писать их в саму локальную файловую систему.

Теперь, имея работающий кэш, мы можем завершить уловку с атрибутами файла. Воспользуемся некоторыми выражениями-условиями, чтобы суметь вывести и атрибуты содержимого корневого каталога, и файлы в каталоге с нашими документами. Мы специально сделали блок условий для записей, которые появляются в нашем каталоге /odt, но, в предположении плоской структуры каталогов, вы можете легко расширить этот сервисный блок на все подкаталоги, изменив условия на проверку пути их трех элементов. Мы также возвращаем корректный код ошибки о несуществующих элементах, чтобы удовлетворить все эти нудные запросы ОС, описанные выше.

  def getattr(self, path):
    st = MyStat()
    st.st_atime = int(time.time())
    st.st_mtime = st.st_atime
    st.st_ctime = st.st_atime
    path_element= path.split(‘/’)
    if path == ‘/’:
       st.st_mode = stat.S_IFDIR | 0755
       st.st_nlink = 2
       st.st_size=4096
    elif path_element[1] == ‘odt’:
       if len(path_element) == 2 :
          #это ката лог
          st.st_mode = stat.S_IFDIR | 0755
          st.st_nlink = 2
       else :
          #это файл
          #он в кэше?
          tpath =os.path.join(cachedir, path[1:])
          if os.path.exists(tpath):
             real=os.stat(tpath)
             st.st_size=real.st_size
          else:
             st.st_size= 2048
          st.st_mode = stat.S_IFREG | 0444
          st.st_nlink = 1
    else:
       return -errno.ENOENT
    return st

Последний метод – чтение файла. Если мы передаем filehandle, то все просто:

 def read(self, path, size, offset=0, filehandle=None):
   filehandle.seek(offset)
   buffer= filehandle.read(size)
   return buffer

Некоторые методы чтения запрашивают конкретный кусок данных, поэтому мы сдвигаем указатель на смещение offset и читаем должный кусок.

Теперь у нас есть работающая файловая система, которую мы можем примонтировать и читать файлы прямо с Google Docs, но ее легко расширить также и на другие типы файлов. Для выгрузки файлов лучше всего создать новый листинг каталога с правами на запись, а затем нам потребуется использовать методы mkdir(), mknod() и write() для создания записей.

Для монтирования виртуального диска необходимо создать пустой каталог, который будет действовать как точка монтирования, затем вызвать приложение с аргументом ваша_точка_монтирования:

python simplefuse.py ваша_точка_монтирования

Теперь вы можете использовать файловую систему в обычных командах или приложениях. Если вы работаете в современном графическом окружении, ваша смонтированная файловая система должна отображаться в виде устройства, или вы можете просто перейти к ней при помощи менеджера файлов.

Вы можете сделать с ней намного больше – все в web, имеющее API, это отличная игра; а не то создайте более абстрактные файловые системы для конвертирования и реорганизации данных. Просто подумайте, и мы уверены, что у вас появятся идеи множества интересных проектов, для воплощения новых навыков. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика