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). 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.
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.
26 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
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
35 For the USB put the interface number: "usb interface".
37 For the TTY put the device name: "tty /dev/ttyUSB0".
39 For the IrDA: just put "IrDA" in the file.
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.
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
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.
69 __date__ = "$Date$"[7:-2]
70 __author__ = "Oleg Broytman <phd@phd.pp.ru>"
71 __copyright__ = "Copyright (C) 2004-2010 PhiloSoft Design"
75 # Change this to suite your needs
76 obexftp_prog = "/usr/bin/obexftp"
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
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)
94 ObexFTP Virtual FileSystem for Midnight Commander version %s
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__
105 tempdirlist = _candidate_tempdir_list()
106 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
109 for tempdir in tempdirlist:
111 logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
112 logfile = open(logfile_name, 'w')
121 logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
124 logger.removeHandler(log_err_handler)
125 logger.addHandler(logging.FileHandler(logfile_name))
127 locale.setlocale(locale.LC_ALL, '')
128 charset = locale.getpreferredencoding()
131 # Parse ObexFTP XML directory listings
133 class DirectoryEntry(object):
134 """Represent a remote file or a directory"""
136 def __init__(self, type):
140 self.perm = "-rw-rw-rw-"
141 elif type == "folder":
142 self.perm = "drwxrwxrwx"
144 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
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)
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)
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)
166 return """<%s: type=directory, name=%s at 0x%x>""" % (
167 self.__class__.__name__, self.name, id(self)
169 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
171 def get_entries(dom, type):
173 for obj in dom.getElementsByTagName(type):
174 entry = DirectoryEntry(type)
175 attrs = obj.attributes
176 for i in range(attrs.length):
178 setattr(entry, attr.name, attr.value)
179 entries.append(entry)
183 # A unique directory for temporary files
188 tmpdir_name = mkdtemp(".tmp", "mcobex-")
189 os.chdir(tmpdir_name)
191 def cleanup_tmpdir():
193 shutil.rmtree(tmpdir_name)
199 s = os.read(fd, 1024)
207 """Run the obexftp binary catching errors"""
209 out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
210 err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
212 command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
213 out_filename, err_filename)
215 logger.debug("Running command %s", command)
218 result = _read(out_fd)
219 os.remove(out_filename)
221 errors = _read(err_fd)
222 os.remove(err_filename)
224 logger.debug(" result: %s", result)
225 logger.debug(" errors: %s", errors)
226 return result, errors
229 def recursive_list(directory='/'):
230 """List the directory recursively"""
231 listing, errors = _run("-l '%s'" % directory)
234 logger.error("Error reading XML listing: %s", errors)
237 dom = xml.dom.minidom.parseString(listing)
238 directories = get_entries(dom, "folder")
239 files = get_entries(dom, "file")
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
247 for entry in directories:
248 fullpath = "%s/%s" % (directory, entry.name)
249 if fullpath.startswith('//'): fullpath = fullpath[1:]
251 recursive_list(fullpath)
254 """List the entire VFS"""
262 def mcobex_copyout():
263 """Get a file from the VFS"""
264 obex_filename = sys.argv[3]
265 real_filename = sys.argv[4]
269 _run("-g '%s'" % obex_filename)
271 os.rename(os.path.basename(obex_filename), real_filename)
273 logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
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)
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
291 logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
297 """Remove a file from the VFS"""
298 obex_filename = sys.argv[3]
300 _run("-k '%s'" % obex_filename)
306 """Create a directory in the VFS"""
307 obex_dirname = sys.argv[3]
309 _run("-C '%s'" % obex_dirname)
314 mcobex_rmdir = mcobex_rm
317 def transport_error(error_str):
318 logger.critical("Error parsing the transport file: %s" % error_str)
321 def setup_transport():
322 """Setup transport parameters for the obexftp program"""
324 transport_file = open(sys.argv[2], 'r')
325 line = transport_file.readline()
326 transport_file.close()
328 transport_error("cannot read '%s'" % sys.argv[2])
330 parts = line.strip().split()
331 transport = parts[0].lower()
333 if transport == "bluetooth":
335 transport_error("not enough arguments for 'bluetooth' transport")
337 transport_error("too many arguments for 'bluetooth' transport")
338 return ' '.join(["-b", parts[1], "-B", parts[2]])
339 elif transport == "usb":
341 transport_error("not enough arguments for 'usb' transport")
343 transport_error("too many arguments for 'usb' transport")
344 return ' '.join(["-u", parts[1]])
345 elif transport == "tty":
347 transport_error("not enough arguments for 'tty' transport")
349 transport_error("too many arguments for 'tty' transport")
350 return ' '.join(["-t", parts[1]])
351 elif transport == "irda":
353 transport_error("too many arguments for 'irda' transport")
356 logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
360 command = sys.argv[1]
361 procname = "mcobex_" + command
364 if not g.has_key(procname):
365 logger.critical("Unknown command %s", command)
370 obexftp_args = setup_transport()
374 logger.exception("Error parsing the transport file")
382 logger.exception("Error during run")