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