phdru.co.cc => phdru.cu.cc
[extfs.d.git] / obexftp
1 #! /usr/bin/env python
2 """ObexFTP Virtual FileSystem for Midnight Commander
3
4 Manipulate a cell phone's filesystem calling obexftp binary. This is a complete
5 user-mode solution, no kernel modules required (unlike SieFS or such). The
6 script implements all commands of Midnight Commander VFS, except for
7 undocumented "run"; anyway there are no runnable files in cell phones. The
8 script is written in Python because I I need to parse XML directory listings
9 from obexftp, and Python is the best of all languages suited for the task ;).
10
11 The script requires Midnight Commander 3.1+ (http://www.ibiblio.org/mc/),
12 Python 2.3+ (http://www.python.org/),
13 OpenOBEX 1.0.1+ (http://openobex.sourceforge.net/) and
14 ObexFTP 0.10.4+ (http://triq.net/obexftp).
15
16 Edit the script, and correct the the full path to the obexftp binary (see
17 obexftp_prog below). For mc 4.7+ put the script in $HOME/.mc/extfs.d.
18 For older versions put it in /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.1"
68 __author__ = "Oleg Broytman <phd@phdru.name>"
69 __copyright__ = "Copyright (C) 2004-2012 PhiloSoft Design"
70 __license__ = "GPL"
71
72
73 # Change this to suite your needs
74 obexftp_prog = "/usr/bin/obexftp"
75
76
77 import sys, os, shutil
78 from time import sleep
79 import xml.dom.minidom, locale
80 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
81
82
83 import logging
84 logger = logging.getLogger('obexftp-mcextfs')
85 log_err_handler = logging.StreamHandler(sys.stderr)
86 logger.addHandler(log_err_handler)
87 logger.setLevel(logging.ERROR)
88
89
90 if len(sys.argv) < 3:
91    logger.critical("""\
92 ObexFTP Virtual FileSystem for Midnight Commander version %s
93 Author: %s
94 %s
95
96 This is not a program. Put the script in /usr/[local/][lib|share]/mc/extfs.
97 For more information read the source!""",
98    __version__, __author__, __copyright__
99 )
100    sys.exit(1)
101
102
103 tempdirlist = _candidate_tempdir_list()
104 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
105
106 found = False
107 for tempdir in tempdirlist:
108    try:
109       logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
110       logfile = open(logfile_name, 'w')
111    except IOError:
112       pass
113    else:
114       found = True
115       logfile.close()
116       break
117
118 if not found:
119    logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
120    sys.exit(1)
121
122 logger.removeHandler(log_err_handler)
123 logger.addHandler(logging.FileHandler(logfile_name))
124
125 locale.setlocale(locale.LC_ALL, '')
126 charset = locale.getpreferredencoding()
127
128
129 # Parse ObexFTP XML directory listings
130
131 class DirectoryEntry(object):
132    """Represent a remote file or a directory"""
133
134    def __init__(self, type):
135       self.type = type
136       self.size = 0
137       if type == "file":
138          self.perm = "-rw-rw-rw-"
139       elif type == "folder":
140          self.perm = "drwxrwxrwx"
141       else:
142          raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
143
144    def mtime(self):
145       if not hasattr(self, "modified"): # telecom
146          return "01-01-70 0:0"
147       date, time = self.modified.split('T')
148       year, month, day = date[2:4], date[4:6], date[6:8]
149       hour, minute = time[:2], time[2:4]
150       return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
151    mtime = property(mtime)
152
153    def __repr__(self):
154       if self.type == "file":
155          return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
156             self.__class__.__name__, self.name, self.size, self.mtime, id(self)
157          )
158       if self.type == "folder":
159          if hasattr(self, "modified"):
160             return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
161                self.__class__.__name__, self.name, self.mtime, id(self)
162             )
163          else: # telecom
164             return """<%s: type=directory, name=%s at 0x%x>""" % (
165                self.__class__.__name__, self.name, id(self)
166             )
167       raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
168
169 def get_entries(dom, type):
170    entries = []
171    for obj in dom.getElementsByTagName(type):
172       entry = DirectoryEntry(type)
173       attrs = obj.attributes
174       for i in range(attrs.length):
175          attr = attrs.item(i)
176          setattr(entry, attr.name, attr.value)
177       entries.append(entry)
178    return entries
179
180
181 # A unique directory for temporary files
182 tmpdir_name = None
183
184 def setup_tmpdir():
185    global tmpdir_name
186    tmpdir_name = mkdtemp(".tmp", "mcobex-")
187    os.chdir(tmpdir_name)
188
189 def cleanup_tmpdir():
190    os.chdir(os.pardir)
191    shutil.rmtree(tmpdir_name)
192
193
194 def _read(fd):
195    out = []
196    while True:
197       s = os.read(fd, 1024)
198       if not s:
199          break
200       out.append(s)
201    return ''.join(out)
202
203
204 def _run(*args):
205    """Run the obexftp binary catching errors"""
206
207    out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
208    err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
209
210    command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
211       out_filename, err_filename)
212
213    logger.debug("Running command %s", command)
214    os.system(command)
215
216    result = _read(out_fd)
217    os.remove(out_filename)
218
219    errors = _read(err_fd)
220    os.remove(err_filename)
221
222    logger.debug("    result: %s", result)
223    logger.debug("    errors: %s", errors)
224    return result, errors
225
226
227 def recursive_list(directory='/'):
228    """List the directory recursively"""
229    listing, errors = _run("-l '%s'" % directory)
230
231    if not listing:
232       logger.error("Error reading XML listing: %s", errors)
233       return
234
235    dom = xml.dom.minidom.parseString(listing)
236    directories = get_entries(dom, "folder")
237    files = get_entries(dom, "file")
238
239    for entry in directories + files:
240       fullpath = "%s/%s" % (directory, entry.name)
241       fullpath = fullpath.encode(charset)
242       if fullpath.startswith('//'): fullpath = fullpath[1:]
243       print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
244
245    for entry in directories:
246       fullpath = "%s/%s" % (directory, entry.name)
247       if fullpath.startswith('//'): fullpath = fullpath[1:]
248       sleep(1)
249       recursive_list(fullpath)
250
251 def mcobex_list():
252    """List the entire VFS"""
253    setup_tmpdir()
254    try:
255       recursive_list()
256    finally:
257       cleanup_tmpdir()
258
259
260 def mcobex_copyout():
261    """Get a file from the VFS"""
262    obex_filename = sys.argv[3]
263    real_filename = sys.argv[4]
264
265    setup_tmpdir()
266    try:
267       _run("-g '%s'" % obex_filename)
268       try:
269          os.rename(os.path.basename(obex_filename), real_filename)
270       except OSError:
271          logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
272    finally:
273       cleanup_tmpdir()
274
275
276 def mcobex_copyin():
277    """Put a file to the VFS"""
278    obex_filename = sys.argv[3]
279    real_filename = sys.argv[4]
280    dirname, filename = os.path.split(obex_filename)
281
282    setup_tmpdir()
283    try:
284       try:
285          os.rename(real_filename, filename)
286          _run("-c '%s' -p '%s'" % (dirname, filename))
287          os.rename(filename, real_filename) # by some reason MC wants the file back
288       except OSError:
289          logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
290    finally:
291       cleanup_tmpdir()
292
293
294 def mcobex_rm():
295    """Remove a file from the VFS"""
296    obex_filename = sys.argv[3]
297    try:
298       _run("-k '%s'" % obex_filename)
299    finally:
300       cleanup_tmpdir()
301
302
303 def mcobex_mkdir():
304    """Create a directory in the VFS"""
305    obex_dirname = sys.argv[3]
306    try:
307       _run("-C '%s'" % obex_dirname)
308    finally:
309       cleanup_tmpdir()
310
311
312 mcobex_rmdir = mcobex_rm
313
314
315 def transport_error(error_str):
316    logger.critical("Error parsing the transport file: %s" % error_str)
317    sys.exit(1)
318
319 def setup_transport():
320    """Setup transport parameters for the obexftp program"""
321    try:
322       transport_file = open(sys.argv[2], 'r')
323       line = transport_file.readline()
324       transport_file.close()
325    except IOError:
326       transport_error("cannot read '%s'" % sys.argv[2])
327
328    parts = line.strip().split()
329    transport = parts[0].lower()
330
331    if transport == "bluetooth":
332       if len(parts) < 3:
333          transport_error("not enough arguments for 'bluetooth' transport")
334       elif len(parts) > 3:
335          transport_error("too many arguments for 'bluetooth' transport")
336       return ' '.join(["-b", parts[1], "-B", parts[2]])
337    elif transport == "usb":
338       if len(parts) < 2:
339          transport_error("not enough arguments for 'usb' transport")
340       elif len(parts) > 2:
341          transport_error("too many arguments for 'usb' transport")
342       return ' '.join(["-u", parts[1]])
343    elif transport == "tty":
344       if len(parts) < 2:
345          transport_error("not enough arguments for 'tty' transport")
346       elif len(parts) > 2:
347          transport_error("too many arguments for 'tty' transport")
348       return ' '.join(["-t", parts[1]])
349    elif transport == "irda":
350       if len(parts) > 1:
351          transport_error("too many arguments for 'irda' transport")
352       return "-i"
353    else:
354       logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
355       sys.exit(1)
356
357
358 command = sys.argv[1]
359 procname = "mcobex_" + command
360
361 g = globals()
362 if not g.has_key(procname):
363    logger.critical("Unknown command %s", command)
364    sys.exit(1)
365
366
367 try:
368    obexftp_args = setup_transport()
369 except SystemExit:
370    raise
371 except:
372    logger.exception("Error parsing the transport file")
373    sys.exit(1)
374
375 try:
376    g[procname]()
377 except SystemExit:
378    raise
379 except:
380    logger.exception("Error during run")