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+ (http://www.ibiblio.org/mc/),
12 Python 2.3+ (http://www.python.org/),
13 OpenOBEX 1.0.1+ (http://dev.zuckschwerdt.org/openobex/) and
14 ObexFTP 0.10.4+ (http://triq.net/obexftp).
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.
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.
68 __author__ = "Oleg Broytman <phd@phdru.name>"
69 __copyright__ = "Copyright (C) 2004-2013 PhiloSoft Design"
73 # Change this to suite your needs
74 obexftp_prog = "/usr/bin/obexftp"
77 import sys, os, shutil
78 from time import sleep
79 import xml.dom.minidom
80 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
89 # Get the default charset.
91 lcAll = locale.getdefaultlocale()
92 except locale.Error, err:
93 print >>sys.stderr, "WARNING:", err
97 default_encoding = lcAll[1]
100 default_encoding = locale.getpreferredencoding()
101 except locale.Error, err:
102 print >>sys.stderr, "WARNING:", err
103 default_encoding = sys.getdefaultencoding()
105 default_encoding = sys.getdefaultencoding()
109 logger = logging.getLogger('obexftp-mcextfs')
110 log_err_handler = logging.StreamHandler(sys.stderr)
111 logger.addHandler(log_err_handler)
112 logger.setLevel(logging.ERROR)
115 if len(sys.argv) < 3:
117 ObexFTP Virtual FileSystem for Midnight Commander version %s
121 This is not a program. Put the script in $HOME/.mc/extfs.d or
122 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
123 __version__, __author__, __copyright__
128 tempdirlist = _candidate_tempdir_list()
129 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
132 for tempdir in tempdirlist:
134 logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
135 logfile = open(logfile_name, 'w')
144 logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
147 logger.removeHandler(log_err_handler)
148 logger.addHandler(logging.FileHandler(logfile_name))
150 locale.setlocale(locale.LC_ALL, '')
153 # Parse ObexFTP XML directory listings
155 class DirectoryEntry(object):
156 """Represent a remote file or a directory"""
158 def __init__(self, type):
162 self.perm = "-rw-rw-rw-"
163 elif type == "folder":
164 self.perm = "drwxrwxrwx"
166 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
169 if not hasattr(self, "modified"): # telecom
170 return "01-01-70 0:0"
171 date, time = self.modified.split('T')
172 year, month, day = date[2:4], date[4:6], date[6:8]
173 hour, minute = time[:2], time[2:4]
174 return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
175 mtime = property(mtime)
178 if self.type == "file":
179 return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
180 self.__class__.__name__, self.name, self.size, self.mtime, id(self)
182 if self.type == "folder":
183 if hasattr(self, "modified"):
184 return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
185 self.__class__.__name__, self.name, self.mtime, id(self)
188 return """<%s: type=directory, name=%s at 0x%x>""" % (
189 self.__class__.__name__, self.name, id(self)
191 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
193 def get_entries(dom, type):
195 for obj in dom.getElementsByTagName(type):
196 entry = DirectoryEntry(type)
197 attrs = obj.attributes
198 for i in range(attrs.length):
200 setattr(entry, attr.name, attr.value)
201 entries.append(entry)
205 # A unique directory for temporary files
210 tmpdir_name = mkdtemp(".tmp", "mcobex-")
211 os.chdir(tmpdir_name)
213 def cleanup_tmpdir():
215 shutil.rmtree(tmpdir_name)
221 s = os.read(fd, 1024)
229 """Run the obexftp binary catching errors"""
231 out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
232 err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
234 command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
235 out_filename, err_filename)
237 logger.debug("Running command %s", command)
240 result = _read(out_fd)
241 os.remove(out_filename)
243 errors = _read(err_fd)
244 os.remove(err_filename)
246 logger.debug(" result: %s", result)
247 logger.debug(" errors: %s", errors)
248 return result, errors
251 def recursive_list(directory='/'):
252 """List the directory recursively"""
253 listing, errors = _run("-l '%s'" % directory)
256 logger.error("Error reading XML listing: %s", errors)
259 dom = xml.dom.minidom.parseString(listing)
260 directories = get_entries(dom, "folder")
261 files = get_entries(dom, "file")
263 for entry in directories + files:
264 fullpath = "%s/%s" % (directory, entry.name)
265 fullpath = fullpath.encode(default_encoding)
266 if fullpath.startswith('//'): fullpath = fullpath[1:]
267 print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
269 for entry in directories:
270 fullpath = "%s/%s" % (directory, entry.name)
271 if fullpath.startswith('//'): fullpath = fullpath[1:]
273 recursive_list(fullpath)
276 """List the entire VFS"""
284 def mcobex_copyout():
285 """Get a file from the VFS"""
286 obex_filename = sys.argv[3]
287 real_filename = sys.argv[4]
291 _run("-g '%s'" % obex_filename)
293 os.rename(os.path.basename(obex_filename), real_filename)
295 logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
301 """Put a file to the VFS"""
302 obex_filename = sys.argv[3]
303 real_filename = sys.argv[4]
304 dirname, filename = os.path.split(obex_filename)
309 os.rename(real_filename, filename)
310 _run("-c '%s' -p '%s'" % (dirname, filename))
311 os.rename(filename, real_filename) # by some reason MC wants the file back
313 logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
319 """Remove a file from the VFS"""
320 obex_filename = sys.argv[3]
322 _run("-k '%s'" % obex_filename)
328 """Create a directory in the VFS"""
329 obex_dirname = sys.argv[3]
331 _run("-C '%s'" % obex_dirname)
336 mcobex_rmdir = mcobex_rm
339 def transport_error(error_str):
340 logger.critical("Error parsing the transport file: %s" % error_str)
343 def setup_transport():
344 """Setup transport parameters for the obexftp program"""
346 transport_file = open(sys.argv[2], 'r')
347 line = transport_file.readline()
348 transport_file.close()
350 transport_error("cannot read '%s'" % sys.argv[2])
352 parts = line.strip().split()
353 transport = parts[0].lower()
355 if transport == "bluetooth":
357 transport_error("not enough arguments for 'bluetooth' transport")
359 transport_error("too many arguments for 'bluetooth' transport")
360 return ' '.join(["-b", parts[1], "-B", parts[2]])
361 elif transport == "usb":
363 transport_error("not enough arguments for 'usb' transport")
365 transport_error("too many arguments for 'usb' transport")
366 return ' '.join(["-u", parts[1]])
367 elif transport == "tty":
369 transport_error("not enough arguments for 'tty' transport")
371 transport_error("too many arguments for 'tty' transport")
372 return ' '.join(["-t", parts[1]])
373 elif transport == "irda":
375 transport_error("too many arguments for 'irda' transport")
378 logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
382 command = sys.argv[1]
383 procname = "mcobex_" + command
386 if not g.has_key(procname):
387 logger.critical("Unknown command %s", command)
392 obexftp_args = setup_transport()
396 logger.exception("Error parsing the transport file")
404 logger.exception("Error during run")