]> git.phdru.name Git - extfs.d.git/blob - obexftp
Replace ~ -> $HOME
[extfs.d.git] / obexftp
1 #! /usr/bin/env python
2 """ObexFTP Virtual FileSystem for Midnight Commander
3
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 ;).
10
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).
15
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.
21
22 See detailed installation instructions at
23 http://phdru.name/Software/mc/INSTALL.html.
24
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.
28
29 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
30 is case-insensitive.
31
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
36 "sdptool browse".
37
38 For the USB put the interface number: "usb interface".
39
40 For the TTY put the device name: "tty /dev/ttyUSB0".
41
42 For the IrDA: just put "IrDA" in the file.
43
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.
52
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
60 it.
61
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.
67
68 """
69
70 __version__ = "1.4.0"
71 __author__ = "Oleg Broytman <phd@phdru.name>"
72 __copyright__ = "Copyright (C) 2004-2013 PhiloSoft Design"
73 __license__ = "GPL"
74
75
76 # Change this to suite your needs
77 obexftp_prog = "/usr/bin/obexftp"
78
79
80 import sys, os, shutil
81 from time import sleep
82 import xml.dom.minidom
83 from tempfile import mkstemp, mkdtemp, _candidate_tempdir_list
84
85 try:
86    import locale
87    use_locale = True
88 except ImportError:
89    use_locale = False
90
91 if use_locale:
92    # Get the default charset.
93    try:
94       lcAll = locale.getdefaultlocale()
95    except locale.Error, err:
96       print >>sys.stderr, "WARNING:", err
97       lcAll = []
98
99    if len(lcAll) == 2:
100       default_encoding = lcAll[1]
101    else:
102       try:
103          default_encoding = locale.getpreferredencoding()
104       except locale.Error, err:
105          print >>sys.stderr, "WARNING:", err
106          default_encoding = sys.getdefaultencoding()
107 else:
108    default_encoding = sys.getdefaultencoding()
109
110
111 import logging
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)
116
117
118 if len(sys.argv) < 3:
119    logger.critical("""\
120 ObexFTP Virtual FileSystem for Midnight Commander version %s
121 Author: %s
122 %s
123
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__
127 )
128    sys.exit(1)
129
130
131 tempdirlist = _candidate_tempdir_list()
132 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
133
134 found = False
135 for tempdir in tempdirlist:
136    try:
137       logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
138       logfile = open(logfile_name, 'w')
139    except IOError:
140       pass
141    else:
142       found = True
143       logfile.close()
144       break
145
146 if not found:
147    logger.critical("Cannot initialize error log file in directories %s" % str(tempdirlist))
148    sys.exit(1)
149
150 logger.removeHandler(log_err_handler)
151 logger.addHandler(logging.FileHandler(logfile_name))
152
153 locale.setlocale(locale.LC_ALL, '')
154
155
156 # Parse ObexFTP XML directory listings
157
158 class DirectoryEntry(object):
159    """Represent a remote file or a directory"""
160
161    def __init__(self, type):
162       self.type = type
163       self.size = 0
164       if type == "file":
165          self.perm = "-rw-rw-rw-"
166       elif type == "folder":
167          self.perm = "drwxrwxrwx"
168       else:
169          raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
170
171    def mtime(self):
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)
179
180    def __repr__(self):
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)
184          )
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)
189             )
190          else: # telecom
191             return """<%s: type=directory, name=%s at 0x%x>""" % (
192                self.__class__.__name__, self.name, id(self)
193             )
194       raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
195
196 def get_entries(dom, type):
197    entries = []
198    for obj in dom.getElementsByTagName(type):
199       entry = DirectoryEntry(type)
200       attrs = obj.attributes
201       for i in range(attrs.length):
202          attr = attrs.item(i)
203          setattr(entry, attr.name, attr.value)
204       entries.append(entry)
205    return entries
206
207
208 # A unique directory for temporary files
209 tmpdir_name = None
210
211 def setup_tmpdir():
212    global tmpdir_name
213    tmpdir_name = mkdtemp(".tmp", "mcobex-")
214    os.chdir(tmpdir_name)
215
216 def cleanup_tmpdir():
217    os.chdir(os.pardir)
218    shutil.rmtree(tmpdir_name)
219
220
221 def _read(fd):
222    out = []
223    while True:
224       s = os.read(fd, 1024)
225       if not s:
226          break
227       out.append(s)
228    return ''.join(out)
229
230
231 def _run(*args):
232    """Run the obexftp binary catching errors"""
233
234    out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
235    err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
236
237    command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
238       out_filename, err_filename)
239
240    logger.debug("Running command %s", command)
241    os.system(command)
242
243    result = _read(out_fd)
244    os.remove(out_filename)
245
246    errors = _read(err_fd)
247    os.remove(err_filename)
248
249    logger.debug("    result: %s", result)
250    logger.debug("    errors: %s", errors)
251    return result, errors
252
253
254 def recursive_list(directory='/'):
255    """List the directory recursively"""
256    listing, errors = _run("-l '%s'" % directory)
257
258    if not listing:
259       logger.error("Error reading XML listing: %s", errors)
260       return
261
262    dom = xml.dom.minidom.parseString(listing)
263    directories = get_entries(dom, "folder")
264    files = get_entries(dom, "file")
265
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
271
272    for entry in directories:
273       fullpath = "%s/%s" % (directory, entry.name)
274       if fullpath.startswith('//'): fullpath = fullpath[1:]
275       sleep(1)
276       recursive_list(fullpath)
277
278 def mcobex_list():
279    """List the entire VFS"""
280    setup_tmpdir()
281    try:
282       recursive_list()
283    finally:
284       cleanup_tmpdir()
285
286
287 def mcobex_copyout():
288    """Get a file from the VFS"""
289    obex_filename = sys.argv[3]
290    real_filename = sys.argv[4]
291
292    setup_tmpdir()
293    try:
294       _run("-g '%s'" % obex_filename)
295       try:
296          os.rename(os.path.basename(obex_filename), real_filename)
297       except OSError:
298          logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
299    finally:
300       cleanup_tmpdir()
301
302
303 def mcobex_copyin():
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)
308
309    setup_tmpdir()
310    try:
311       try:
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
315       except OSError:
316          logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
317    finally:
318       cleanup_tmpdir()
319
320
321 def mcobex_rm():
322    """Remove a file from the VFS"""
323    obex_filename = sys.argv[3]
324    try:
325       _run("-k '%s'" % obex_filename)
326    finally:
327       cleanup_tmpdir()
328
329
330 def mcobex_mkdir():
331    """Create a directory in the VFS"""
332    obex_dirname = sys.argv[3]
333    try:
334       _run("-C '%s'" % obex_dirname)
335    finally:
336       cleanup_tmpdir()
337
338
339 mcobex_rmdir = mcobex_rm
340
341
342 def transport_error(error_str):
343    logger.critical("Error parsing the transport file: %s" % error_str)
344    sys.exit(1)
345
346 def setup_transport():
347    """Setup transport parameters for the obexftp program"""
348    try:
349       transport_file = open(sys.argv[2], 'r')
350       line = transport_file.readline()
351       transport_file.close()
352    except IOError:
353       transport_error("cannot read '%s'" % sys.argv[2])
354
355    parts = line.strip().split()
356    transport = parts[0].lower()
357
358    if transport == "bluetooth":
359       if len(parts) < 3:
360          transport_error("not enough arguments for 'bluetooth' transport")
361       elif len(parts) > 3:
362          transport_error("too many arguments for 'bluetooth' transport")
363       return ' '.join(["-b", parts[1], "-B", parts[2]])
364    elif transport == "usb":
365       if len(parts) < 2:
366          transport_error("not enough arguments for 'usb' transport")
367       elif len(parts) > 2:
368          transport_error("too many arguments for 'usb' transport")
369       return ' '.join(["-u", parts[1]])
370    elif transport == "tty":
371       if len(parts) < 2:
372          transport_error("not enough arguments for 'tty' transport")
373       elif len(parts) > 2:
374          transport_error("too many arguments for 'tty' transport")
375       return ' '.join(["-t", parts[1]])
376    elif transport == "irda":
377       if len(parts) > 1:
378          transport_error("too many arguments for 'irda' transport")
379       return "-i"
380    else:
381       logger.critical("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
382       sys.exit(1)
383
384
385 command = sys.argv[1]
386 procname = "mcobex_" + command
387
388 g = globals()
389 if not g.has_key(procname):
390    logger.critical("Unknown command %s", command)
391    sys.exit(1)
392
393
394 try:
395    obexftp_args = setup_transport()
396 except SystemExit:
397    raise
398 except:
399    logger.exception("Error parsing the transport file")
400    sys.exit(1)
401
402 try:
403    g[procname]()
404 except SystemExit:
405    raise
406 except:
407    logger.exception("Error during run")