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://openobex.sourceforge.net/) 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-2012 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, locale
80 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
84 logger = logging.getLogger('obexftp-mcextfs')
85 log_err_handler = logging.StreamHandler(sys.stderr)
86 logger.addHandler(log_err_handler)
87 logger.setLevel(logging.ERROR)
92 ObexFTP Virtual FileSystem for Midnight Commander version %s
96 This is not a program. Put the script in /usr/[local/][lib|share]/mc/extfs.
97 For more information read the source!""",
98 __version__, __author__, __copyright__
103 tempdirlist = _candidate_tempdir_list()
104 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
107 for tempdir in tempdirlist:
109 logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
110 logfile = open(logfile_name, 'w')
119 logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
122 logger.removeHandler(log_err_handler)
123 logger.addHandler(logging.FileHandler(logfile_name))
125 locale.setlocale(locale.LC_ALL, '')
126 charset = locale.getpreferredencoding()
129 # Parse ObexFTP XML directory listings
131 class DirectoryEntry(object):
132 """Represent a remote file or a directory"""
134 def __init__(self, type):
138 self.perm = "-rw-rw-rw-"
139 elif type == "folder":
140 self.perm = "drwxrwxrwx"
142 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
145 if not hasattr(self, "modified"): # telecom
146 return "01-01-70 0:0"
147 date, time = self.modified.split('T')
148 year, month, day = date[2:4], date[4:6], date[6:8]
149 hour, minute = time[:2], time[2:4]
150 return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
151 mtime = property(mtime)
154 if self.type == "file":
155 return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
156 self.__class__.__name__, self.name, self.size, self.mtime, id(self)
158 if self.type == "folder":
159 if hasattr(self, "modified"):
160 return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
161 self.__class__.__name__, self.name, self.mtime, id(self)
164 return """<%s: type=directory, name=%s at 0x%x>""" % (
165 self.__class__.__name__, self.name, id(self)
167 raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
169 def get_entries(dom, type):
171 for obj in dom.getElementsByTagName(type):
172 entry = DirectoryEntry(type)
173 attrs = obj.attributes
174 for i in range(attrs.length):
176 setattr(entry, attr.name, attr.value)
177 entries.append(entry)
181 # A unique directory for temporary files
186 tmpdir_name = mkdtemp(".tmp", "mcobex-")
187 os.chdir(tmpdir_name)
189 def cleanup_tmpdir():
191 shutil.rmtree(tmpdir_name)
197 s = os.read(fd, 1024)
205 """Run the obexftp binary catching errors"""
207 out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
208 err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
210 command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
211 out_filename, err_filename)
213 logger.debug("Running command %s", command)
216 result = _read(out_fd)
217 os.remove(out_filename)
219 errors = _read(err_fd)
220 os.remove(err_filename)
222 logger.debug(" result: %s", result)
223 logger.debug(" errors: %s", errors)
224 return result, errors
227 def recursive_list(directory='/'):
228 """List the directory recursively"""
229 listing, errors = _run("-l '%s'" % directory)
232 logger.error("Error reading XML listing: %s", errors)
235 dom = xml.dom.minidom.parseString(listing)
236 directories = get_entries(dom, "folder")
237 files = get_entries(dom, "file")
239 for entry in directories + files:
240 fullpath = "%s/%s" % (directory, entry.name)
241 fullpath = fullpath.encode(charset)
242 if fullpath.startswith('//'): fullpath = fullpath[1:]
243 print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
245 for entry in directories:
246 fullpath = "%s/%s" % (directory, entry.name)
247 if fullpath.startswith('//'): fullpath = fullpath[1:]
249 recursive_list(fullpath)
252 """List the entire VFS"""
260 def mcobex_copyout():
261 """Get a file from the VFS"""
262 obex_filename = sys.argv[3]
263 real_filename = sys.argv[4]
267 _run("-g '%s'" % obex_filename)
269 os.rename(os.path.basename(obex_filename), real_filename)
271 logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
277 """Put a file to the VFS"""
278 obex_filename = sys.argv[3]
279 real_filename = sys.argv[4]
280 dirname, filename = os.path.split(obex_filename)
285 os.rename(real_filename, filename)
286 _run("-c '%s' -p '%s'" % (dirname, filename))
287 os.rename(filename, real_filename) # by some reason MC wants the file back
289 logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
295 """Remove a file from the VFS"""
296 obex_filename = sys.argv[3]
298 _run("-k '%s'" % obex_filename)
304 """Create a directory in the VFS"""
305 obex_dirname = sys.argv[3]
307 _run("-C '%s'" % obex_dirname)
312 mcobex_rmdir = mcobex_rm
315 def transport_error(error_str):
316 logger.critical("Error parsing the transport file: %s" % error_str)
319 def setup_transport():
320 """Setup transport parameters for the obexftp program"""
322 transport_file = open(sys.argv[2], 'r')
323 line = transport_file.readline()
324 transport_file.close()
326 transport_error("cannot read '%s'" % sys.argv[2])
328 parts = line.strip().split()
329 transport = parts[0].lower()
331 if transport == "bluetooth":
333 transport_error("not enough arguments for 'bluetooth' transport")
335 transport_error("too many arguments for 'bluetooth' transport")
336 return ' '.join(["-b", parts[1], "-B", parts[2]])
337 elif transport == "usb":
339 transport_error("not enough arguments for 'usb' transport")
341 transport_error("too many arguments for 'usb' transport")
342 return ' '.join(["-u", parts[1]])
343 elif transport == "tty":
345 transport_error("not enough arguments for 'tty' transport")
347 transport_error("too many arguments for 'tty' transport")
348 return ' '.join(["-t", parts[1]])
349 elif transport == "irda":
351 transport_error("too many arguments for 'irda' transport")
354 logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
358 command = sys.argv[1]
359 procname = "mcobex_" + command
362 if not g.has_key(procname):
363 logger.critical("Unknown command %s", command)
368 obexftp_args = setup_transport()
372 logger.exception("Error parsing the transport file")
380 logger.exception("Error during run")