Version 1.2.1 (2004-10-03). Put error logfile in the same directory as the transport...
[extfs.d.git] / obexftp
1 #! /usr/local/bin/python -O
2
3 """
4 ObexFTP Virtual FileSystem for Midnight Commander.
5
6 Author: Oleg BroytMann <phd@phd.pp.ru>.
7 Copyright (C) 2004 PhiloSoft Design.
8 License: GPL.
9
10 Manipulate a cell phone's filesystem calling obexftp binary. This is a
11 complete user-mode solution, no kernel modules required (unlike SieFS or
12 such). The script implements all commands of Midnight VFS, except for
13 undocumented "run"; anyway there are no runnable files in the cell phone. The
14 script is written in Python because I love Python, the best of all languages ;),
15 and I need to parse XML directory listings from obexftp.
16
17 The script requires Midnight Commander 3.1+ (http://www.ibiblio.org/mc/),
18 Python 2.3+ (http://www.python.org/),
19 OpenOBEX 1.0.1+ (http://openobex.sourceforge.net/) and
20 ObexFTP 0.10.4+ (http://triq.net/obexftp).
21
22 Edit the script, and correct the shebang path, if your python is not in the
23 /usr/local. Edit the full path to the obexftp binary (see below). Put the file
24 in the /usr/[local/]lib/mc/extfs, and add a line "obexftp" to the
25 /usr/[local/]lib/mc/extfs/extfs.ini.
26
27 Create somewhere a transport file. The transport file may have any name, and
28 is expected to be a text file with at least one line defining the transport to
29 your device. Other lines in the file are ignored.
30
31 First word in the line is a transport name - Bluetooth, TTY or IrDA. The name
32 is case-insensitive.
33
34 For the Bluetooth transport put there a line "Bluetooth CP:AD:RE:SS channel",
35 where CP:AD:RE:SS is the hardware address of the device you want to connect
36 to, and "channel" is the OBEX File Transfer channel; you can discover the
37 address and the channel for your device by using commands like "hcitool scan"
38 and "sdptool browse".
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
45 the command is "%cd"): cd transport#obexftp, where "transport" is the name of
46 your transport file. The script uses obexftp to connect to the device and list
47 files and directories. Please be warned that opening the VFS for the first
48 time is VERY slow, because the script needs to scan the entire cell phone's
49 filesystem. And there must be timeouts between connections, which don't make
50 the scanning faster. Midnight Commander caches the result so you can browse
51 and manipulate 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 disagreement between the VFS
55 cache and the phone. It is not very dangerous but inconvenient. There is no
56 way to clear the VFS cache in Midnight Commander and reread the filesystem.
57 You 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
60 restart it.
61
62 If something goes wrong set the logging level (see setLevel() below) to INFO
63 or 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
66 is named in $TMP environment variable.
67 """
68
69 __version__ = "1.2.1"
70 __revision__ = "$Id: obexftp,v 1.18 2004/10/03 13:33:03 phd Exp $"
71 __date__ = "$Date: 2004/10/03 13:33:03 $"[7:-2]
72 __author__ = "Oleg Broytmann <phd@phd.pp.ru>"
73 __copyright__ = "Copyright (C) 2004 PhiloSoft Design"
74
75
76 # Change this to suite your needs
77 obexftp_prog = "/usr/local/obex/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
86 import logging
87 logger = logging.getLogger('obexftp-mcextfs')
88 log_err_handler = logging.StreamHandler(sys.stderr)
89 logger.addHandler(log_err_handler)
90 logger.setLevel(logging.ERROR)
91
92
93 if len(sys.argv) < 3:
94    logger.error("""\
95 ObexFTP Virtual FileSystem for Midnight Commander version %s
96 Author: %s
97 %s
98
99 This is not a program. It is ObexFTP Virtual FileSystem for Midnight Commander.
100 Put it in /usr/lib/mc/extfs. For more information read the source!""",
101    __version__, __author__, __copyright__
102 )
103    sys.exit(1)
104
105
106 tempdirlist = _candidate_tempdir_list()
107 tempdirlist.insert(0, os.path.abspath(os.path.dirname(sys.argv[2])))
108
109 found = False
110 for tempdir in tempdirlist:
111    try:
112       logfile_name = os.path.join(tempdir, 'obexftp-mcextfs.log')
113       logfile = open(logfile_name, 'w')
114    except IOError:
115       pass
116    else:
117       found = True
118       logfile.close()
119       break
120
121 if not found:
122    logger.error("Cannot initialize error log file in directories %s" % str(tempdirlist))
123    sys.exit(1)
124
125 logger.removeHandler(log_err_handler)
126 logger.addHandler(logging.FileHandler(logfile_name))
127
128
129 # Parse ObexFTP XML directory listings
130
131 class DirectoryEntry(object):
132    """Represent a remote file or a directory"""
133
134    def __init__(self, type):
135       self.type = type
136       self.size = 0
137       if type == "file":
138          self.perm = "-rw-rw-rw-"
139       elif type == "folder":
140          self.perm = "drwxrwxrwx"
141       else:
142          raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
143
144    def mtime(self):
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)
152
153    def __repr__(self):
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)
157          )
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)
162             )
163          else: # telecom
164             return """<%s: type=directory, name=%s at 0x%x>""" % (
165                self.__class__.__name__, self.name, id(self)
166             )
167       raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
168
169 def get_entries(dom, type):
170    entries = []
171    for obj in dom.getElementsByTagName(type):
172       entry = DirectoryEntry(type)
173       attrs = obj.attributes
174       for i in range(attrs.length):
175          attr = attrs.item(i)
176          setattr(entry, attr.name, attr.value)
177       entries.append(entry)
178    return entries
179
180
181 # A unique directory for temporary files
182 tmpdir_name = None
183
184 def setup_tmpdir():
185    global tmpdir_name
186    tmpdir_name = mkdtemp(".tmp", "mcobex-")
187    os.chdir(tmpdir_name)
188
189 def cleanup_tmpdir():
190    os.chdir(os.pardir)
191    shutil.rmtree(tmpdir_name)
192
193
194 def _read(fd):
195    out = []
196    while True:
197       s = os.read(fd, 1024)
198       if not s:
199          break
200       out.append(s)
201    return ''.join(out)
202
203
204 def _run(*args):
205    """Run the obexftp binary catching errors"""
206
207    out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
208    err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
209
210    command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
211       out_filename, err_filename)
212
213    logger.debug("Running command %s", command)
214    os.system(command)
215
216    result = _read(out_fd)
217    os.remove(out_filename)
218
219    errors = _read(err_fd)
220    os.remove(err_filename)
221
222    logger.debug("    result: %s", result)
223    logger.debug("    errors: %s", errors)
224    return result, errors
225
226
227 def recursive_list(directory='/'):
228    """List the directory recursively"""
229    listing, errors = _run("-l '%s'" % directory)
230
231    if not listing:
232       logger.error("Error reading XML listing: %s", errors)
233       return
234
235    dom = xml.dom.minidom.parseString(listing)
236    directories = get_entries(dom, "folder")
237    files = get_entries(dom, "file")
238
239    for entry in directories + files:
240       fullpath = "%s/%s" % (directory, entry.name)
241       if fullpath.startswith('//'): fullpath = fullpath[1:]
242       print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
243
244    for entry in directories:
245       fullpath = "%s/%s" % (directory, entry.name)
246       if fullpath.startswith('//'): fullpath = fullpath[1:]
247       sleep(1)
248       recursive_list(fullpath)
249
250 def mcobex_list():
251    """List the entire VFS"""
252    setup_tmpdir()
253    try:
254       recursive_list()
255    finally:
256       cleanup_tmpdir()
257
258
259 def mcobex_copyout():
260    """Get a file from the VFS"""
261    obex_filename = sys.argv[3]
262    real_filename = sys.argv[4]
263
264    setup_tmpdir()
265    try:
266       _run("-g '%s'" % obex_filename)
267       try:
268          os.rename(os.path.basename(obex_filename), real_filename)
269       except OSError:
270          logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
271    finally:
272       cleanup_tmpdir()
273
274
275 def mcobex_copyin():
276    """Put a file to the VFS"""
277    obex_filename = sys.argv[3]
278    real_filename = sys.argv[4]
279    dirname, filename = os.path.split(obex_filename)
280
281    setup_tmpdir()
282    try:
283       try:
284          os.rename(real_filename, filename)
285          _run("-c '%s' -p '%s'" % (dirname, filename))
286          os.rename(filename, real_filename) # by some reason MC wants the file back
287       except OSError:
288          logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
289    finally:
290       cleanup_tmpdir()
291
292
293 def mcobex_rm():
294    """Remove a file from the VFS"""
295    obex_filename = sys.argv[3]
296    try:
297       _run("-k '%s'" % obex_filename)
298    finally:
299       cleanup_tmpdir()
300
301
302 def mcobex_mkdir():
303    """Create a directory in the VFS"""
304    obex_dirname = sys.argv[3]
305    try:
306       _run("-C '%s'" % obex_dirname)
307    finally:
308       cleanup_tmpdir()
309
310
311 mcobex_rmdir = mcobex_rm
312
313
314 def transport_error(error_str):
315    logger.error("Error parsing the transport file: %s" % error_str)
316    sys.exit(1)
317
318 def setup_transport():
319    """Setup transport parameters for the obexftp program"""
320    try:
321       transport_file = open(sys.argv[2], 'r')
322       line = transport_file.readline()
323       transport_file.close()
324    except IOError:
325       transport_error("cannot read '%s'" % sys.argv[2])
326
327    parts = line.strip().split()
328    transport = parts[0].lower()
329
330    if transport == "bluetooth":
331       if len(parts) < 3:
332          transport_error("not enough arguments for 'bluetooth' transport")
333       elif len(parts) > 3:
334          transport_error("too many arguments for 'bluetooth' transport")
335       return ' '.join(["-b", parts[1], "-B", parts[2]])
336    elif transport == "tty":
337       if len(parts) < 2:
338          transport_error("not enough arguments for 'tty' transport")
339       elif len(parts) > 2:
340          transport_error("too many arguments for 'tty' transport")
341       return ' '.join(["-t", parts[1]])
342    elif transport == "irda":
343       if len(parts) > 1:
344          transport_error("too many arguments for 'irda' transport")
345       return "-i"
346    else:
347       logger.error("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
348       sys.exit(1)
349
350
351 command = sys.argv[1]
352 procname = "mcobex_" + command
353
354 g = globals()
355 if not g.has_key(procname):
356    logger.error("Unknown command %s", command)
357    sys.exit(1)
358
359
360 try:
361    obexftp_args = setup_transport()
362 except SystemExit:
363    raise
364 except:
365    logger.exception("Error parsing the transport file")
366    sys.exit(1)
367
368 try:
369    g[procname]()
370 except SystemExit:
371    raise
372 except:
373    logger.exception("Error during run")