2 # -*- coding: utf-8 -*-
5 # Midnight Commander compatible EXTFS for accessing Amazon Web Services S3.
6 # Written by Jakob Kemi <jakob.kemi@gmail.com> 2009
8 # Copyright (c) 2009 Free Software Foundation, Inc.
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # This EXTFS exposes buckets as directories and keys as files
26 # Due to EXTFS limitations all buckets & keys have to be read initially which might
27 # take quite some time.
28 # Tested on Debian with Python 2.4-2.6 and boto 1.4c and 1.6b
29 # (Python 2.6 might need -W ignore::DeprecationWarning due to boto using
30 # deprecated module Popen2)
34 # Make sure that boto <http://code.google.com/p/boto> (python-boto in Debian) is installed.
35 # Preferably pytz (package python-tz in Debian) should be installed as well.
37 # Save as executable file /share/mc/extfs/s3 (or wherever your mc expects to find extfs modules)
38 # Add the the following to your extfs.ini (might exists as /usr/share/mc/extfs/extfs.ini):
39 # ----- begin extfs.ini -----
42 # ----- end extfs.ini -----
45 # Settings: (should be set via environment)
47 # AWS_ACCESS_KEY_ID : Amazon AWS acces key (required)
48 # AWS_SECRET_ACCESS_KEY : Amazon AWS secret access key (required)
50 # MCVFS_EXTFS_S3_LOCATION : where to create new buckets, "EU"(default) or "US"
51 # MCVFS_EXTFS_S3_DEBUGFILE : write debug info to this file (no info default)
55 # Open dialog "Quick cd" (<alt-c>) and type: #s3 <enter> (or simply type ''cd #s3'' in shell line)
59 # 2009-02-07 Jakob Kemi <jakob.kemi@gmail.com>
60 # - Updated instructions.
61 # - Improved error reporting.
63 # 2009-02-06 Jakob Kemi <jakob.kemi@gmail.com>
64 # - Threaded list command.
65 # - Handle rm of empty "subdirectories" (as seen in mc).
66 # - List most recent datetime and total size of keys as directory properties.
67 # - List modification time in local time.
69 # 2009-02-05 Jakob Kemi <jakob.kemi@gmail.com>
81 from boto
.s3
.connection
import S3Connection
82 from boto
.s3
.key
import Key
83 from boto
.exception
import BotoServerError
86 # Get settings from environment
87 USER
=os
.getenv('USER','0')
88 AWS_ACCESS_KEY_ID
=os
.getenv('AWS_ACCESS_KEY_ID')
89 AWS_SECRET_ACCESS_KEY
=os
.getenv('AWS_SECRET_ACCESS_KEY')
90 LOCATION
= os
.getenv('MCVFS_EXTFS_S3_LOCATION', 'EU').lower()
91 DEBUGFILE
= os
.getenv('MCVFS_EXTFS_S3_DEBUGFILE')
93 if not AWS_ACCESS_KEY_ID
or not AWS_SECRET_ACCESS_KEY
:
94 sys
.stderr
.write('Missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variables.\n')
103 format
='%(asctime)s %(levelname)s %(message)s')
104 logging
.getLogger('boto').setLevel(logging
.WARNING
)
107 def __getattr__(self
, attr
):
109 def __call__(self
, *args
, **kw
):
113 logger
=logging
.getLogger('s3extfs')
116 def threadmap(fun
, iterable
, maxthreads
=16):
118 Quick and dirty threaded version of builtin method map.
119 Propagates exception safely.
121 from threading
import Thread
124 items
= list(iterable
)
127 return map(fun
, items
)
129 # Create and fill input queue
130 input = Queue
.Queue()
131 output
= Queue
.Queue()
133 for i
,item
in enumerate(items
):
134 input.put( (i
,item
) )
136 class WorkThread(Thread
):
138 Takes one item from input queue (thread terminates when input queue is empty),
139 performs fun, puts result in output queue
144 (i
,item
) = input.get_nowait()
147 output
.put( (i
,result
) )
149 output
.put( (None,sys
.exc_info()) )
154 for i
in range( min(len(items
), maxthreads
) ):
159 # Wait for all threads to finish & collate results
161 for i
in range(nitems
):
165 raise res
[0],res
[1],res
[2]
172 logger
.debug('started')
174 # Global S3 connection
175 s3
= S3Connection(AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
)
177 logger
.debug('Using location EU for new buckets')
178 S3LOCATION
= boto
.s3
.connection
.Location
.EU
180 logger
.debug('Using location US for new buckets')
181 S3LOCATION
= boto
.s3
.connection
.Location
.US
183 logger
.debug('argv: ' + str(sys
.argv
))
189 sys
.stderr
.write('This program should be called from within MC\n')
192 def handleServerError(msg
):
194 msg
+= ', reason: ' + e
[1].reason
195 logger
.error(msg
, exc_info
=e
)
196 sys
.stderr
.write(msg
+'\n')
200 # Lists all S3 contents
210 rs
= s3
.get_all_buckets()
212 # Import python timezones (pytz)
216 logger
.warning('Missing pytz module, timestamps will be off')
217 # A fallback UTC tz stub
218 class pytzutc(datetime
.tzinfo
):
220 datetime
.tzinfo
.__init
__(self
)
223 def utcoffset(self
, dt
):
224 return datetime
.timedelta(0)
225 def tzname(self
, dt
):
228 return datetime
.timedelta(0)
233 # (yes, timeZONE as in _geographic zone_ not EST/CEST or whatever crap we get from time.tzname)
234 # http://regebro.wordpress.com/2008/05/10/python-and-time-zones-part-2-the-beast-returns/
235 def getGuessedTimezone():
236 # 1. check TZ env. var
238 tz
= os
.getenv('TZ', '')
239 return pytz
.timezone(tz
)
242 # 2. check if /etc/timezone exists (Debian at least)
244 if os
.path
.isfile('/etc/timezone'):
245 tz
= open('/etc/timezone', 'r').readline().strip()
246 return pytz
.timezone(tz
)
249 # 3. check if /etc/localtime is a _link_ to something useful
251 if os
.path
.islink('/etc/localtime'):
252 link
= os
.readlink('/etc/localtime')
253 tz
= '/'.join(p
.split(os
.path
.sep
)[-2:])
254 return pytz
.timezone(tz
)
257 # 4. use time.tzname which will probably be wrong by an hour 50% of the time.
259 return pytz
.timezone(time
.tzname
[0])
262 # 5. use plain UTC ...
265 tz
=getGuessedTimezone()
266 logger
.debug('Using timezone: ' + tz
.zone
)
268 # AWS time is on format: 2009-01-07T16:43:39.000Z
269 # we "want" MM-DD-YYYY hh:mm (in localtime)
270 expr
= re
.compile(r
'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.\d{3}Z$')
271 def convDate(awsdatetime
):
272 m
= expr
.match(awsdatetime
)
273 ye
,mo
,da
,ho
,mi
,se
= map(int,m
.groups())
275 dt
= datetime
.datetime(ye
,mo
,da
,ho
,mi
,se
, tzinfo
=pytz
.utc
)
276 return dt
.astimezone(tz
).strftime('%m-%d-%Y %H:%M')
281 mostrecent
= '1970-01-01T00:00:00.000Z'
284 mostrecent
= max(mostrecent
, k
.last_modified
)
285 datetime
= convDate(k
.last_modified
)
286 ret
.append('%10s %3d %-8s %-8s %d %s %s\n' % (
287 '-rw-r--r--', 1, USER
, USER
, k
.size
, datetime
, b
.name
+'/'+k
.name
)
291 datetime
=convDate(mostrecent
)
292 sys
.stdout
.write('%10s %3d %-8s %-8s %d %s %s\n' % (
293 'drwxr-xr-x', 1, USER
, USER
, totsz
, datetime
, b
.name
)
296 sys
.stdout
.write(line
)
298 threadmap(bucketList
, rs
)
303 elif cmd
== 'copyout':
304 archivename
= args
[0]
305 storedfilename
= args
[1]
308 bucket
,key
= storedfilename
.split('/', 1)
309 logger
.info('copyout bucket: %s, key: %s'%(bucket
, key
))
312 b
= s3
.get_bucket(bucket
)
315 out
= open(extractto
, 'w')
322 except BotoServerError
:
323 handleServerError('Unable to fetch key "%s"'%(key))
328 elif cmd
== 'copyin':
329 archivename
= args
[0]
330 storedfilename
= args
[1]
333 bucket
,key
= storedfilename
.split('/', 1)
334 logger
.info('copyin bucket: %s, key: %s'%(bucket
, key
))
337 b
= s3
.get_bucket(bucket
)
339 k
.set_contents_from_file(fp
=open(sourcefile
,'r'))
340 except BotoServerError
:
341 handleServerError('Unable to upload key "%s"' % (key
))
344 # Remove file from S3
347 archivename
= args
[0]
348 storedfilename
= args
[1]
350 bucket
,key
= storedfilename
.split('/', 1)
351 logger
.info('rm bucket: %s, key: %s'%(bucket
, key
))
354 b
= s3
.get_bucket(bucket
)
356 except BotoServerError
:
357 handleServerError('Unable to remove key "%s"' % (key
))
363 archivename
= args
[0]
366 logger
.info('mkdir dir: %s' %(dirname))
368 logger
.warning('skipping mkdir')
373 s3
.create_bucket(bucket
, location
=boto
.s3
.connection
.Location
.EU
)
374 except BotoServerError
:
375 handleServerError('Unable to create bucket "%s"' % (bucket
))
381 archivename
= args
[0]
384 logger
.info('rmdir dir: %s' %(dirname))
386 logger
.warning('skipping rmdir')
391 b
= s3
.get_bucket(bucket
)
393 except BotoServerError
:
394 handleServerError('Unable to delete bucket "%s"' % (bucket
))
400 archivename
= args
[0]
401 storedfilename
= args
[1]
404 bucket
,key
= storedfilename
.split('/', 1)
405 logger
.info('run bucket: %s, key: %s'%(bucket
, key
))
407 os
.execv(storedfilename
, arguments
)
409 logger
.error('unhandled, bye')
412 logger
.debug('command handled')