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