Read-only filesystem.
[extfs.d.git] / obexftp
1 #! /usr/bin/env python
2
3 """ObexFTP Virtual FileSystem for Midnight Commander
4
5 Manipulate a cell phone's filesystem calling obexftp binary. This is a complete
6 user-mode solution, no kernel modules required (unlike SieFS or such). The
7 script implements all commands of Midnight Commander VFS, except for
8 undocumented "run"; anyway there are no runnable files in cell phones. The
9 script is written in Python because I I need to parse XML directory listings
10 from obexftp, and Python is the best of all languages suited for the task ;).
11
12 The script requires Midnight Commander 3.1+ (http://www.ibiblio.org/mc/),
13 Python 2.3+ (http://www.python.org/),
14 OpenOBEX 1.0.1+ (http://openobex.sourceforge.net/) and
15 ObexFTP 0.10.4+ (http://triq.net/obexftp).
16
17 Edit the script, and correct the the full path to the obexftp binary (see
18 obexftp_prog below). Put the script in the /usr/[local/][lib|share]/mc/extfs,
19 and add a line "obexftp" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
20 Make the script executable.
21
22 Create somewhere a transport file. The transport file may have any name, and is
23 expected to be a text file with at least one line defining the transport to
24 your device. Other lines in the file are ignored.
25
26 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
27 is case-insensitive.
28
29 For the Bluetooth transport put there a line "Bluetooth CP:AD:RE:SS channel",
30 where CP:AD:RE:SS is the hardware address of the device you want to connect to,
31 and "channel" is the OBEX File Transfer channel; you can discover the address
32 and the channel for your device by using commands like "hcitool scan" and
33 "sdptool browse".
34
35 For the USB put the interface number: "usb interface".
36
37 For the TTY put the device name: "tty /dev/ttyUSB0".
38
39 For the IrDA: just put "IrDA" in the file.
40
41 Now run this "cd" command in the Midnight Commander (in the "bindings" file the
42 command is "%cd"): cd transport#obexftp, where "transport" is the name of your
43 transport file. The script uses obexftp to connect to the device and list files
44 and directories. Please be warned that opening the VFS for the first time is
45 VERY slow, because the script needs to scan the entire cell phone's filesystem,
46 and there are timeouts between connections, which don't make the scanning
47 faster. Midnight Commander caches the result so you can browse and manipulate
48 files and directories quickly.
49
50 Please note that manipulating the filesystem using your phone's internal
51 filemanager in parallel with the VFS leads to a disagreement between the VFS
52 cache and the phone. It is not very dangerous but inconvenient. There is no way
53 to clear the VFS cache in Midnight Commander and reread the filesystem. You
54 have to exit the VFS (cd /, for example) and return back using cd
55 transport#obexftp command. Sometimes even this doesn't help - Midnight
56 Commander shows the same cached VFS image. Exit Midnight Commander and restart
57 it.
58
59 If something goes wrong set the logging level (see setLevel() below) to INFO or
60 DEBUG and look in the obexftp-mcextfs.log file. The file is put in the same
61 directory as the transport file, if it possible; if not the file will be put
62 into a temporary directory, usually /tmp, or /var/tmp, or whatever directory is
63 named in $TMP environment variable.
64
65 """
66
67 __version__ = "1.3.0"
68 __revision__ = "$Id$"
69 __date__ = "$Date$"[7:-2]
70 __author__ = "Oleg Broytman <phd@phd.pp.ru>"
71 __copyright__ = "Copyright (C) 2004-2010 PhiloSoft Design"
72 __license__ = "GPL"
73
74
75 # Change this to suite your needs
76 obexftp_prog = "/usr/bin/obexftp"
77
78
79 import sys, os, shutil
80 from time import sleep
81 import xml.dom.minidom, locale
82 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
83
84
85 import logging
86 logger = logging.getLogger('obexftp-mcextfs')
87 log_err_handler = logging.StreamHandler(sys.stderr)
88 logger.addHandler(log_err_handler)
89 logger.setLevel(logging.ERROR)
90
91
92 if len(sys.argv) < 3:
93    logger.critical("""\
94 ObexFTP Virtual FileSystem for Midnight Commander version %s
95 Author: %s
96 %s
97
98 This is not a program. Put the script in /usr/[local/][lib|share]/mc/extfs.
99 For more information read the source!""",
100    __version__, __author__, __copyright__
101 )
102    sys.exit(1)
103
104
105 tempdirlist = _candidate_tempdir_list()
106 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
107
108 found = False
109 for tempdir in tempdirlist:
110    try:
111       logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
112       logfile = open(logfile_name, 'w')
113    except IOError:
114       pass
115    else:
116       found = True
117       logfile.close()
118       break
119
120 if not found:
121    logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
122    sys.exit(1)
123
124 logger.removeHandler(log_err_handler)
125 logger.addHandler(logging.FileHandler(logfile_name))
126
127 locale.setlocale(locale.LC_ALL, '')
128 charset = locale.getpreferredencoding()
129
130
131 # Parse ObexFTP XML directory listings
132
133 class DirectoryEntry(object):
134    """Represent a remote file or a directory"""
135
136    def __init__(self, type):
137       self.type = type
138       self.size = 0
139       if type == "file":
140          self.perm = "-rw-rw-rw-"
141       elif type == "folder":
142          self.perm = "drwxrwxrwx"
143       else:
144          raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
145
146    def mtime(self):
147       if not hasattr(self, "modified"): # telecom
148          return "01-01-70 0:0"
149       date, time = self.modified.split('T')
150       year, month, day = date[2:4], date[4:6], date[6:8]
151       hour, minute = time[:2], time[2:4]
152       return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
153    mtime = property(mtime)
154
155    def __repr__(self):
156       if self.type == "file":
157          return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
158             self.__class__.__name__, self.name, self.size, self.mtime, id(self)
159          )
160       if self.type == "folder":
161          if hasattr(self, "modified"):
162             return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
163                self.__class__.__name__, self.name, self.mtime, id(self)
164             )
165          else: # telecom
166             return """<%s: type=directory, name=%s at 0x%x>""" % (
167                self.__class__.__name__, self.name, id(self)
168             )
169       raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
170
171 def get_entries(dom, type):
172    entries = []
173    for obj in dom.getElementsByTagName(type):
174       entry = DirectoryEntry(type)
175       attrs = obj.attributes
176       for i in range(attrs.length):
177          attr = attrs.item(i)
178          setattr(entry, attr.name, attr.value)
179       entries.append(entry)
180    return entries
181
182
183 # A unique directory for temporary files
184 tmpdir_name = None
185
186 def setup_tmpdir():
187    global tmpdir_name
188    tmpdir_name = mkdtemp(".tmp", "mcobex-")
189    os.chdir(tmpdir_name)
190
191 def cleanup_tmpdir():
192    os.chdir(os.pardir)
193    shutil.rmtree(tmpdir_name)
194
195
196 def _read(fd):
197    out = []
198    while True:
199       s = os.read(fd, 1024)
200       if not s:
201          break
202       out.append(s)
203    return ''.join(out)
204
205
206 def _run(*args):
207    """Run the obexftp binary catching errors"""
208
209    out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
210    err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
211
212    command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
213       out_filename, err_filename)
214
215    logger.debug("Running command %s", command)
216    os.system(command)
217
218    result = _read(out_fd)
219    os.remove(out_filename)
220
221    errors = _read(err_fd)
222    os.remove(err_filename)
223
224    logger.debug("    result: %s", result)
225    logger.debug("    errors: %s", errors)
226    return result, errors
227
228
229 def recursive_list(directory='/'):
230    """List the directory recursively"""
231    listing, errors = _run("-l '%s'" % directory)
232
233    if not listing:
234       logger.error("Error reading XML listing: %s", errors)
235       return
236
237    dom = xml.dom.minidom.parseString(listing)
238    directories = get_entries(dom, "folder")
239    files = get_entries(dom, "file")
240
241    for entry in directories + files:
242       fullpath = "%s/%s" % (directory, entry.name)
243       fullpath = fullpath.encode(charset)
244       if fullpath.startswith('//'): fullpath = fullpath[1:]
245       print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
246
247    for entry in directories:
248       fullpath = "%s/%s" % (directory, entry.name)
249       if fullpath.startswith('//'): fullpath = fullpath[1:]
250       sleep(1)
251       recursive_list(fullpath)
252
253 def mcobex_list():
254    """List the entire VFS"""
255    setup_tmpdir()
256    try:
257       recursive_list()
258    finally:
259       cleanup_tmpdir()
260
261
262 def mcobex_copyout():
263    """Get a file from the VFS"""
264    obex_filename = sys.argv[3]
265    real_filename = sys.argv[4]
266
267    setup_tmpdir()
268    try:
269       _run("-g '%s'" % obex_filename)
270       try:
271          os.rename(os.path.basename(obex_filename), real_filename)
272       except OSError:
273          logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
274    finally:
275       cleanup_tmpdir()
276
277
278 def mcobex_copyin():
279    """Put a file to the VFS"""
280    obex_filename = sys.argv[3]
281    real_filename = sys.argv[4]
282    dirname, filename = os.path.split(obex_filename)
283
284    setup_tmpdir()
285    try:
286       try:
287          os.rename(real_filename, filename)
288          _run("-c '%s' -p '%s'" % (dirname, filename))
289          os.rename(filename, real_filename) # by some reason MC wants the file back
290       except OSError:
291          logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
292    finally:
293       cleanup_tmpdir()
294
295
296 def mcobex_rm():
297    """Remove a file from the VFS"""
298    obex_filename = sys.argv[3]
299    try:
300       _run("-k '%s'" % obex_filename)
301    finally:
302       cleanup_tmpdir()
303
304
305 def mcobex_mkdir():
306    """Create a directory in the VFS"""
307    obex_dirname = sys.argv[3]
308    try:
309       _run("-C '%s'" % obex_dirname)
310    finally:
311       cleanup_tmpdir()
312
313
314 mcobex_rmdir = mcobex_rm
315
316
317 def transport_error(error_str):
318    logger.critical("Error parsing the transport file: %s" % error_str)
319    sys.exit(1)
320
321 def setup_transport():
322    """Setup transport parameters for the obexftp program"""
323    try:
324       transport_file = open(sys.argv[2], 'r')
325       line = transport_file.readline()
326       transport_file.close()
327    except IOError:
328       transport_error("cannot read '%s'" % sys.argv[2])
329
330    parts = line.strip().split()
331    transport = parts[0].lower()
332
333    if transport == "bluetooth":
334       if len(parts) < 3:
335          transport_error("not enough arguments for 'bluetooth' transport")
336       elif len(parts) > 3:
337          transport_error("too many arguments for 'bluetooth' transport")
338       return ' '.join(["-b", parts[1], "-B", parts[2]])
339    elif transport == "usb":
340       if len(parts) < 2:
341          transport_error("not enough arguments for 'usb' transport")
342       elif len(parts) > 2:
343          transport_error("too many arguments for 'usb' transport")
344       return ' '.join(["-u", parts[1]])
345    elif transport == "tty":
346       if len(parts) < 2:
347          transport_error("not enough arguments for 'tty' transport")
348       elif len(parts) > 2:
349          transport_error("too many arguments for 'tty' transport")
350       return ' '.join(["-t", parts[1]])
351    elif transport == "irda":
352       if len(parts) > 1:
353          transport_error("too many arguments for 'irda' transport")
354       return "-i"
355    else:
356       logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
357       sys.exit(1)
358
359
360 command = sys.argv[1]
361 procname = "mcobex_" + command
362
363 g = globals()
364 if not g.has_key(procname):
365    logger.critical("Unknown command %s", command)
366    sys.exit(1)
367
368
369 try:
370    obexftp_args = setup_transport()
371 except SystemExit:
372    raise
373 except:
374    logger.exception("Error parsing the transport file")
375    sys.exit(1)
376
377 try:
378    g[procname]()
379 except SystemExit:
380    raise
381 except:
382    logger.exception("Error during run")