2 """ObexFTP Virtual FileSystem for Midnight Commander
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 ;).
11 The script requires Midnight Commander 3.1+
12 (http://www.midnight-commander.org/), Python 2.3+ (http://www.python.org/),
13 OpenOBEX 1.0.1+ (http://dev.zuckschwerdt.org/openobex/) and ObexFTP 0.10.4+
14 (http://triq.net/obexftp).
16 Edit the script, and correct the full path to the obexftp binary (see
17 obexftp_prog below). For mc 4.7+ put the script in
18 $HOME/[.local/share/].mc/extfs.d. For older versions put it in
19 /usr/[local/][lib|share]/mc/extfs and add a line "obexftp" to the
20 /usr/[local/][lib|share]/mc/extfs/extfs.ini. Make the script executable.
22 See detailed installation instructions at
23 https://phdru.name/Software/mc/INSTALL.html.
25 Create somewhere a transport file. The transport file may have any name, and is
26 expected to be a text file with at least one line defining the transport to
27 your device. Other lines in the file are ignored.
29 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
32 For the Bluetooth transport put there a line "Bluetooth CP:AD:RE:SS channel",
33 where CP:AD:RE:SS is the hardware address of the device you want to connect to,
34 and "channel" is the OBEX File Transfer channel; you can discover the address
35 and the channel for your device by using commands like "hcitool scan" and
38 For the USB put the interface number: "usb interface".
40 For the TTY put the device name: "tty /dev/ttyUSB0".
42 For the IrDA: just put "IrDA" in the file.
44 Now run this "cd" command in the Midnight Commander (in the "bindings" file the
45 command is "%cd"): cd transport#obexftp, where "transport" is the name of your
46 transport file. The script uses obexftp to connect to the device and list files
47 and directories. Please be warned that opening the VFS for the first time is
48 VERY slow, because the script needs to scan the entire cell phone's filesystem,
49 and there are timeouts between connections, which don't make the scanning
50 faster. Midnight Commander caches the result so you can browse and manipulate
51 files and directories quickly.
53 Please note that manipulating the filesystem using your phone's internal
54 filemanager in parallel with the VFS leads to a disagreement between the VFS
55 cache and the phone. It is not very dangerous but inconvenient. There is no way
56 to clear the VFS cache in Midnight Commander and reread the filesystem. You
57 have to exit the VFS (cd /, for example) and return back using cd
58 transport#obexftp command. Sometimes even this doesn't help - Midnight
59 Commander shows the same cached VFS image. Exit Midnight Commander and restart
62 If something goes wrong set the logging level (see setLevel() below) to INFO or
63 DEBUG and look in the obexftp-mcextfs.log file. The file is put in the same
64 directory as the transport file, if it possible; if not the file will be put
65 into a temporary directory, usually /tmp, or /var/tmp, or whatever directory is
66 named in $TMP environment variable.
71 __author__ = "Oleg Broytman <phd@phdru.name>"
72 __copyright__ = "Copyright (C) 2004-2013 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
83 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
92 # Get the default charset.
94 lcAll = locale.getdefaultlocale()
95 except locale.Error, err:
96 print >>sys.stderr, "WARNING:", err
100 default_encoding = lcAll[1]
103 default_encoding = locale.getpreferredencoding()
104 except locale.Error, err:
105 print >>sys.stderr, "WARNING:", err
106 default_encoding = sys.getdefaultencoding()
108 default_encoding = sys.getdefaultencoding()
112 logger = logging.getLogger('obexftp-mcextfs')
113 log_err_handler = logging.StreamHandler(sys.stderr)
114 logger.addHandler(log_err_handler)
115 logger.setLevel(logging.ERROR)
118 if len(sys.argv) < 3:
120 ObexFTP Virtual FileSystem for Midnight Commander version %s
124 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
125 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
126 __version__, __author__, __copyright__
131 tempdirlist = _candidate_tempdir_list()
132 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
135 for tempdir in tempdirlist:
137 logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
138 logfile = open(logfile_name, 'w')
147 logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
150 logger.removeHandler(log_err_handler)
151 logger.addHandler(logging.FileHandler(logfile_name))
153 locale.setlocale(locale.LC_ALL, '')
156 # Parse ObexFTP XML directory listings
158 class DirectoryEntry(object):
159 """Represent a remote file or a directory"""
161 def __init__(self, type):
165 self.perm = "-rw-rw-rw-"
166 elif type == "folder":
167 self.perm = "drwxrwxrwx"
169 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
172 if not hasattr(self, "modified"): # telecom
173 return "01-01-70 0:0"
174 date, time = self.modified.split('T')
175 year, month, day = date[2:4], date[4:6], date[6:8]
176 hour, minute = time[:2], time[2:4]
177 return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
178 mtime = property(mtime)
181 if self.type == "file":
182 return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
183 self.__class__.__name__, self.name, self.size, self.mtime, id(self)
185 if self.type == "folder":
186 if hasattr(self, "modified"):
187 return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
188 self.__class__.__name__, self.name, self.mtime, id(self)
191 return """<%s: type=directory, name=%s at 0x%x>""" % (
192 self.__class__.__name__, self.name, id(self)
194 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
196 def get_entries(dom, type):
198 for obj in dom.getElementsByTagName(type):
199 entry = DirectoryEntry(type)
200 attrs = obj.attributes
201 for i in range(attrs.length):
203 setattr(entry, attr.name, attr.value)
204 entries.append(entry)
208 # A unique directory for temporary files
213 tmpdir_name = mkdtemp(".tmp", "mcobex-")
214 os.chdir(tmpdir_name)
216 def cleanup_tmpdir():
218 shutil.rmtree(tmpdir_name)
224 s = os.read(fd, 1024)
232 """Run the obexftp binary catching errors"""
234 out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
235 err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
237 command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
238 out_filename, err_filename)
240 logger.debug("Running command %s", command)
243 result = _read(out_fd)
244 os.remove(out_filename)
246 errors = _read(err_fd)
247 os.remove(err_filename)
249 logger.debug(" result: %s", result)
250 logger.debug(" errors: %s", errors)
251 return result, errors
254 def recursive_list(directory='/'):
255 """List the directory recursively"""
256 listing, errors = _run("-l '%s'" % directory)
259 logger.error("Error reading XML listing: %s", errors)
262 dom = xml.dom.minidom.parseString(listing)
263 directories = get_entries(dom, "folder")
264 files = get_entries(dom, "file")
266 for entry in directories + files:
267 fullpath = "%s/%s" % (directory, entry.name)
268 fullpath = fullpath.encode(default_encoding)
269 if fullpath.startswith('//'): fullpath = fullpath[1:]
270 print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
272 for entry in directories:
273 fullpath = "%s/%s" % (directory, entry.name)
274 if fullpath.startswith('//'): fullpath = fullpath[1:]
276 recursive_list(fullpath)
279 """List the entire VFS"""
287 def mcobex_copyout():
288 """Get a file from the VFS"""
289 obex_filename = sys.argv[3]
290 real_filename = sys.argv[4]
294 _run("-g '%s'" % obex_filename)
296 os.rename(os.path.basename(obex_filename), real_filename)
298 logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
304 """Put a file to the VFS"""
305 obex_filename = sys.argv[3]
306 real_filename = sys.argv[4]
307 dirname, filename = os.path.split(obex_filename)
312 os.rename(real_filename, filename)
313 _run("-c '%s' -p '%s'" % (dirname, filename))
314 os.rename(filename, real_filename) # by some reason MC wants the file back
316 logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
322 """Remove a file from the VFS"""
323 obex_filename = sys.argv[3]
325 _run("-k '%s'" % obex_filename)
331 """Create a directory in the VFS"""
332 obex_dirname = sys.argv[3]
334 _run("-C '%s'" % obex_dirname)
339 mcobex_rmdir = mcobex_rm
342 def transport_error(error_str):
343 logger.critical("Error parsing the transport file: %s" % error_str)
346 def setup_transport():
347 """Setup transport parameters for the obexftp program"""
349 transport_file = open(sys.argv[2], 'r')
350 line = transport_file.readline()
351 transport_file.close()
353 transport_error("cannot read '%s'" % sys.argv[2])
355 parts = line.strip().split()
356 transport = parts[0].lower()
358 if transport == "bluetooth":
360 transport_error("not enough arguments for 'bluetooth' transport")
362 transport_error("too many arguments for 'bluetooth' transport")
363 return ' '.join(["-b", parts[1], "-B", parts[2]])
364 elif transport == "usb":
366 transport_error("not enough arguments for 'usb' transport")
368 transport_error("too many arguments for 'usb' transport")
369 return ' '.join(["-u", parts[1]])
370 elif transport == "tty":
372 transport_error("not enough arguments for 'tty' transport")
374 transport_error("too many arguments for 'tty' transport")
375 return ' '.join(["-t", parts[1]])
376 elif transport == "irda":
378 transport_error("too many arguments for 'irda' transport")
381 logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
385 command = sys.argv[1]
386 procname = "mcobex_" + command
389 if not g.has_key(procname):
390 logger.critical("Unknown command %s", command)
395 obexftp_args = setup_transport()
399 logger.exception("Error parsing the transport file")
407 logger.exception("Error during run")