3 """ObexFTP Virtual FileSystem for Midnight Commander
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 ;).
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).
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.
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.
27 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
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
36 For the USB put the interface number: "usb interface".
38 For the TTY put the device name: "tty /dev/ttyUSB0".
40 For the IrDA: just put "IrDA" in the file.
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.
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
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.
70 __date__ = "$Date$"[7:-2]
71 __author__ = "Oleg Broytman <phd@phdru.name>"
72 __copyright__ = "Copyright (C) 2004-2011 PhiloSoft Design"
76 # Change this to suite your needs
77 obexftp_prog = "/usr/bin/obexftp"
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
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)
95 ObexFTP Virtual FileSystem for Midnight Commander version %s
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__
106 tempdirlist = _candidate_tempdir_list()
107 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
110 for tempdir in tempdirlist:
112 logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
113 logfile = open(logfile_name, 'w')
122 logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
125 logger.removeHandler(log_err_handler)
126 logger.addHandler(logging.FileHandler(logfile_name))
128 locale.setlocale(locale.LC_ALL, '')
129 charset = locale.getpreferredencoding()
132 # Parse ObexFTP XML directory listings
134 class DirectoryEntry(object):
135 """Represent a remote file or a directory"""
137 def __init__(self, type):
141 self.perm = "-rw-rw-rw-"
142 elif type == "folder":
143 self.perm = "drwxrwxrwx"
145 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
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)
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)
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)
167 return """<%s: type=directory, name=%s at 0x%x>""" % (
168 self.__class__.__name__, self.name, id(self)
170 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
172 def get_entries(dom, type):
174 for obj in dom.getElementsByTagName(type):
175 entry = DirectoryEntry(type)
176 attrs = obj.attributes
177 for i in range(attrs.length):
179 setattr(entry, attr.name, attr.value)
180 entries.append(entry)
184 # A unique directory for temporary files
189 tmpdir_name = mkdtemp(".tmp", "mcobex-")
190 os.chdir(tmpdir_name)
192 def cleanup_tmpdir():
194 shutil.rmtree(tmpdir_name)
200 s = os.read(fd, 1024)
208 """Run the obexftp binary catching errors"""
210 out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
211 err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
213 command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
214 out_filename, err_filename)
216 logger.debug("Running command %s", command)
219 result = _read(out_fd)
220 os.remove(out_filename)
222 errors = _read(err_fd)
223 os.remove(err_filename)
225 logger.debug(" result: %s", result)
226 logger.debug(" errors: %s", errors)
227 return result, errors
230 def recursive_list(directory='/'):
231 """List the directory recursively"""
232 listing, errors = _run("-l '%s'" % directory)
235 logger.error("Error reading XML listing: %s", errors)
238 dom = xml.dom.minidom.parseString(listing)
239 directories = get_entries(dom, "folder")
240 files = get_entries(dom, "file")
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
248 for entry in directories:
249 fullpath = "%s/%s" % (directory, entry.name)
250 if fullpath.startswith('//'): fullpath = fullpath[1:]
252 recursive_list(fullpath)
255 """List the entire VFS"""
263 def mcobex_copyout():
264 """Get a file from the VFS"""
265 obex_filename = sys.argv[3]
266 real_filename = sys.argv[4]
270 _run("-g '%s'" % obex_filename)
272 os.rename(os.path.basename(obex_filename), real_filename)
274 logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
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)
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
292 logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
298 """Remove a file from the VFS"""
299 obex_filename = sys.argv[3]
301 _run("-k '%s'" % obex_filename)
307 """Create a directory in the VFS"""
308 obex_dirname = sys.argv[3]
310 _run("-C '%s'" % obex_dirname)
315 mcobex_rmdir = mcobex_rm
318 def transport_error(error_str):
319 logger.critical("Error parsing the transport file: %s" % error_str)
322 def setup_transport():
323 """Setup transport parameters for the obexftp program"""
325 transport_file = open(sys.argv[2], 'r')
326 line = transport_file.readline()
327 transport_file.close()
329 transport_error("cannot read '%s'" % sys.argv[2])
331 parts = line.strip().split()
332 transport = parts[0].lower()
334 if transport == "bluetooth":
336 transport_error("not enough arguments for 'bluetooth' transport")
338 transport_error("too many arguments for 'bluetooth' transport")
339 return ' '.join(["-b", parts[1], "-B", parts[2]])
340 elif transport == "usb":
342 transport_error("not enough arguments for 'usb' transport")
344 transport_error("too many arguments for 'usb' transport")
345 return ' '.join(["-u", parts[1]])
346 elif transport == "tty":
348 transport_error("not enough arguments for 'tty' transport")
350 transport_error("too many arguments for 'tty' transport")
351 return ' '.join(["-t", parts[1]])
352 elif transport == "irda":
354 transport_error("too many arguments for 'irda' transport")
357 logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
361 command = sys.argv[1]
362 procname = "mcobex_" + command
365 if not g.has_key(procname):
366 logger.critical("Unknown command %s", command)
371 obexftp_args = setup_transport()
375 logger.exception("Error parsing the transport file")
383 logger.exception("Error during run")