]> git.phdru.name Git - extfs.d.git/blob - obexftp
Fixed a major bug with blocked read from a pipe. Now I use files instead of pipes.
[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(s).
64
65 """
66
67 __version__ = "1.2.0"
68 __revision__ = "$Id: obexftp,v 1.17 2004/09/25 16:13:39 phd Exp $"
69 __date__ = "$Date: 2004/09/25 16:13:39 $"[7:-2]
70 __author__ = "Oleg Broytmann <phd@phd.pp.ru>"
71 __copyright__ = "Copyright (C) 2004 PhiloSoft Design"
72
73
74 # Change this to suite your needs
75 obexftp_prog = "/usr/local/obex/bin/obexftp"
76
77
78 import sys, time
79 import os, shutil
80 import xml.dom.minidom
81 from tempfile import mkstemp, mkdtemp
82
83
84 import logging
85 logger = logging.getLogger('obexftp-mcextfs')
86 logger.addHandler(logging.FileHandler('obexftp-mcextfs.log'))
87 logger.setLevel(logging.ERROR)
88
89
90 if len(sys.argv) < 2:
91    logger.error("""\
92 ObexFTP Virtual FileSystem for Midnight Commander version %s
93 Author: %s
94 %s
95 Put it in /usr/lib/mc/extfs. For more information read the source!""",
96    __version__, __author__, __copyright__
97 )
98    sys.exit(1)
99
100
101 # Parse ObexFTP XML directory listings
102
103 class DirectoryEntry(object):
104    """Represent a remote file or a directory"""
105
106    def __init__(self, type):
107       self.type = type
108       self.size = 0
109       if type == "file":
110          self.perm = "-rw-rw-rw-"
111       elif type == "folder":
112          self.perm = "drwxrwxrwx"
113       else:
114          raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
115
116    def mtime(self):
117       if not hasattr(self, "modified"): # telecom
118          return "01-01-70 0:0"
119       date, time = self.modified.split('T')
120       year, month, day = date[2:4], date[4:6], date[6:8]
121       hour, minute = time[:2], time[2:4]
122       return "%s-%s-%s %s:%s" % (month, day, year, hour, minute)
123    mtime = property(mtime)
124
125    def __repr__(self):
126       if self.type == "file":
127          return """<%s: type=file, name=%s, size=%s, mtime=%s at 0x%x>""" % (
128             self.__class__.__name__, self.name, self.size, self.mtime, id(self)
129          )
130       if self.type == "folder":
131          if hasattr(self, "modified"):
132             return """<%s: type=directory, name=%s, mtime=%s at 0x%x>""" % (
133                self.__class__.__name__, self.name, self.mtime, id(self)
134             )
135          else: # telecom
136             return """<%s: type=directory, name=%s at 0x%x>""" % (
137                self.__class__.__name__, self.name, id(self)
138             )
139       raise ValueError, "unknown type '%s'; expected 'file' or 'folder'" % self.type
140
141 def get_entries(dom, type):
142    entries = []
143    for obj in dom.getElementsByTagName(type):
144       entry = DirectoryEntry(type)
145       attrs = obj.attributes
146       for i in range(attrs.length):
147          attr = attrs.item(i)
148          setattr(entry, attr.name, attr.value)
149       entries.append(entry)
150    return entries
151
152
153 # A unique directory for temporary files
154 tmpdir_name = None
155
156 def setup_tmpdir():
157    global tmpdir_name
158    tmpdir_name = mkdtemp(".tmp", "mcobex-")
159    os.chdir(tmpdir_name)
160
161 def cleanup_tmpdir():
162    os.chdir(os.pardir)
163    shutil.rmtree(tmpdir_name)
164
165
166 def _read(fd):
167    out = []
168    while True:
169       s = os.read(fd, 1024)
170       if not s:
171          break
172       out.append(s)
173    return ''.join(out)
174
175
176 def _run(*args):
177    """Run the obexftp binary catching errors"""
178
179    out_fd, out_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
180    err_fd, err_filename = mkstemp(".tmp", "mcobex-", tmpdir_name)
181
182    command = "%s %s %s >%s 2>%s" % (obexftp_prog, obexftp_args, ' '.join(args),
183       out_filename, err_filename)
184
185    logger.debug("Running command %s", command)
186    os.system(command)
187
188    result = _read(out_fd)
189    os.remove(out_filename)
190
191    errors = _read(err_fd)
192    os.remove(err_filename)
193
194    logger.debug("    result: %s", result)
195    logger.debug("    errors: %s", errors)
196    return result, errors
197
198
199 def recursive_list(directory='/'):
200    """List the directory recursively"""
201    listing, errors = _run("-l '%s'" % directory)
202
203    if not listing:
204       logger.error("Error reading XML listing: %s", errors)
205       return
206
207    dom = xml.dom.minidom.parseString(listing)
208    directories = get_entries(dom, "folder")
209    files = get_entries(dom, "file")
210
211    for entry in directories + files:
212       fullpath = "%s/%s" % (directory, entry.name)
213       if fullpath.startswith('//'): fullpath = fullpath[1:]
214       print entry.perm, "1 user group", entry.size, entry.mtime, fullpath
215
216    for entry in directories:
217       fullpath = "%s/%s" % (directory, entry.name)
218       if fullpath.startswith('//'): fullpath = fullpath[1:]
219       time.sleep(1)
220       recursive_list(fullpath)
221
222 def mcobex_list():
223    """List the entire VFS"""
224    setup_tmpdir()
225    try:
226       recursive_list()
227    finally:
228       cleanup_tmpdir()
229
230
231 def mcobex_copyout():
232    """Get a file from the VFS"""
233    obex_filename = sys.argv[3]
234    real_filename = sys.argv[4]
235
236    setup_tmpdir()
237    try:
238       _run("-g '%s'" % obex_filename)
239       try:
240          os.rename(os.path.basename(obex_filename), real_filename)
241       except OSError:
242          logger.exception("Error CopyOut %s to %s", obex_filename, real_filename)
243    finally:
244       cleanup_tmpdir()
245
246
247 def mcobex_copyin():
248    """Put a file to the VFS"""
249    obex_filename = sys.argv[3]
250    real_filename = sys.argv[4]
251    dirname, filename = os.path.split(obex_filename)
252
253    setup_tmpdir()
254    try:
255       try:
256          os.rename(real_filename, filename)
257          _run("-c '%s' -p '%s'" % (dirname, filename))
258          os.rename(filename, real_filename) # by some reason MC wants the file back
259       except OSError:
260          logger.exception("Error CopyIn %s to %s", real_filename, obex_filename)
261    finally:
262       cleanup_tmpdir()
263
264
265 def mcobex_rm():
266    """Remove a file from the VFS"""
267    obex_filename = sys.argv[3]
268    try:
269       _run("-k '%s'" % obex_filename)
270    finally:
271       cleanup_tmpdir()
272
273
274 def mcobex_mkdir():
275    """Create a directory in the VFS"""
276    obex_dirname = sys.argv[3]
277    try:
278       _run("-C '%s'" % obex_dirname)
279    finally:
280       cleanup_tmpdir()
281
282
283 mcobex_rmdir = mcobex_rm
284
285
286 def transport_error(error_str):
287    logger.error("Error parsing the transport file: %s" % error_str)
288    sys.exit(1)
289
290 def setup_transport():
291    """Setup transport parameters for the obexftp program"""
292    try:
293       transport_file = open(sys.argv[2], 'r')
294       line = transport_file.readline()
295       transport_file.close()
296    except IOError:
297       transport_error("cannot read '%s'" % sys.argv[2])
298
299    parts = line.strip().split()
300    transport = parts[0].lower()
301
302    if transport == "bluetooth":
303       if len(parts) < 3:
304          transport_error("not enough arguments for 'bluetooth' transport")
305       elif len(parts) > 3:
306          transport_error("too many arguments for 'bluetooth' transport")
307       return ' '.join(["-b", parts[1], "-B", parts[2]])
308    elif transport == "tty":
309       if len(parts) < 2:
310          transport_error("not enough arguments for 'tty' transport")
311       elif len(parts) > 2:
312          transport_error("too many arguments for 'tty' transport")
313       return ' '.join(["-t", parts[1]])
314    elif transport == "irda":
315       if len(parts) > 1:
316          transport_error("too many arguments for 'irda' transport")
317       return "-i"
318    else:
319       logger.error("Unknown transport '%s'; expected 'bluetooth', 'tty' or 'irda'", transport)
320       sys.exit(1)
321
322
323 command = sys.argv[1]
324 procname = "mcobex_" + command
325
326 g = globals()
327 if not g.has_key(procname):
328    logger.error("Unknown command %s", command)
329    sys.exit(1)
330
331
332 try:
333    obexftp_args = setup_transport()
334 except SystemExit:
335    raise
336 except:
337    logger.exception("Error parsing the transport file")
338    sys.exit(1)
339
340 try:
341    g[procname]()
342 except SystemExit:
343    raise
344 except:
345    logger.exception("Error during run")