3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 """App Engine Files API."""
23 from __future__
import with_statement
27 'ApiTemporaryUnavailableError',
28 'BLOBSTORE_FILESYSTEM',
30 'ExclusiveLockFailedError',
33 'FileTemporaryUnavailableError',
37 'InvalidArgumentError',
38 'InvalidFileNameError',
39 'InvalidParameterError',
40 'OperationNotSupportedError',
41 'PermissionDeniedError',
43 'SequenceKeyOutOfOrderError',
45 'UnsupportedContentTypeError',
46 'UnsupportedOpenModeError',
47 'WrongContentTypeError' ,
66 from google
.appengine
.api
import apiproxy_stub_map
67 from google
.appengine
.api
.files
import file_service_pb
68 from google
.appengine
.runtime
import apiproxy_errors
71 BLOBSTORE_FILESYSTEM
= 'blobstore'
73 FILESYSTEMS
= (BLOBSTORE_FILESYSTEM
, GS_FILESYSTEM
)
74 READ_BLOCK_SIZE
= 1024 * 512
75 _CREATION_HANDLE_PREFIX
= 'writable:'
76 _DEFAULT_BUFFER_SIZE
= 512 * 1024
79 class Error(Exception):
80 """Base error class for this module."""
83 class UnsupportedOpenModeError(Error
):
84 """Unsupported file open mode was specified."""
87 class UnsupportedContentTypeError(Error
):
88 """Specified file content type is not supported by this api."""
91 class InvalidArgumentError(Error
):
92 """Function argument has invalid value."""
95 class FinalizationError(Error
):
96 """File is in wrong finalization state."""
99 class ExistenceError(Error
):
100 """File is in wrong existence state."""
103 class UnknownError(Error
):
104 """Unknown unexpected io error occured."""
107 class SequenceKeyOutOfOrderError(Error
):
108 """Sequence key specified is out of order.
111 last_sequence_key: last sequence key which was written to the file.
114 def __init__(self
, last_sequence_key
, cause
=None):
115 Error
.__init
__(self
, cause
)
116 self
.last_sequence_key
= last_sequence_key
119 class InvalidFileNameError(Error
):
120 """File name is invalid."""
123 class FileNotOpenedError(Error
):
124 """File was not opened."""
127 class ReadOnlyError(Error
):
128 """File is read-only mode."""
131 class WrongContentTypeError(Error
):
132 """File has a different content type."""
135 class WrongOpenModeError(Error
):
136 """Incorrect file open mode."""
139 class OperationNotSupportedError(Error
):
140 """Incorrect file open mode."""
143 class PermissionDeniedError(Error
):
144 """Application doesn't have permissions to perform the operation."""
147 class ApiTemporaryUnavailableError(Error
):
148 """Files API is temporary unavailable. Request should be retried soon."""
151 class FileTemporaryUnavailableError(Error
):
152 """File is temporary unavailable. Request should be retried soon."""
155 class InvalidParameterError(Error
):
156 """Parameter specified in Create() call is invalid."""
159 class ExclusiveLockFailedError(Error
):
160 """Exclusive lock can't be obtained."""
164 RAW
= file_service_pb
.FileContentType
.RAW
167 def _raise_app_error(e
):
168 """Convert RPC error into api-specific exception."""
169 if (e
.application_error
in
170 [file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR
,
171 file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR_METADATA_NOT_FOUND
,
172 file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR_METADATA_FOUND
,
173 file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR_SHARDING_MISMATCH
,
174 file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR_OBJECT_NOT_FOUND
,
175 file_service_pb
.FileServiceErrors
.EXISTENCE_ERROR_BUCKET_NOT_FOUND
,
177 raise ExistenceError(e
)
178 elif (e
.application_error
==
179 file_service_pb
.FileServiceErrors
.API_TEMPORARILY_UNAVAILABLE
):
180 raise ApiTemporaryUnavailableError(e
)
181 elif (e
.application_error
==
182 file_service_pb
.FileServiceErrors
.FINALIZATION_ERROR
):
183 raise FinalizationError(e
)
184 elif (e
.application_error
==
185 file_service_pb
.FileServiceErrors
.IO_ERROR
):
186 raise UnknownError(e
)
187 elif (e
.application_error
==
188 file_service_pb
.FileServiceErrors
.SEQUENCE_KEY_OUT_OF_ORDER
):
189 raise SequenceKeyOutOfOrderError(e
.error_detail
, e
)
190 elif (e
.application_error
==
191 file_service_pb
.FileServiceErrors
.INVALID_FILE_NAME
):
192 raise InvalidFileNameError(e
)
193 elif (e
.application_error
==
194 file_service_pb
.FileServiceErrors
.FILE_NOT_OPENED
):
195 raise FileNotOpenedError(e
)
196 elif (e
.application_error
==
197 file_service_pb
.FileServiceErrors
.READ_ONLY
):
198 raise ReadOnlyError(e
)
199 elif (e
.application_error
==
200 file_service_pb
.FileServiceErrors
.WRONG_CONTENT_TYPE
):
201 raise WrongContentTypeError(e
)
202 elif (e
.application_error
==
203 file_service_pb
.FileServiceErrors
.WRONG_OPEN_MODE
):
204 raise WrongOpenModeError(e
)
205 elif (e
.application_error
==
206 file_service_pb
.FileServiceErrors
.OPERATION_NOT_SUPPORTED
):
207 raise OperationNotSupportedError(e
)
208 elif (e
.application_error
==
209 file_service_pb
.FileServiceErrors
.PERMISSION_DENIED
):
210 raise PermissionDeniedError(e
)
211 elif (e
.application_error
==
212 file_service_pb
.FileServiceErrors
.FILE_TEMPORARILY_UNAVAILABLE
):
213 raise FileTemporaryUnavailableError(e
)
214 elif (e
.application_error
==
215 file_service_pb
.FileServiceErrors
.INVALID_PARAMETER
):
216 raise InvalidParameterError(e
)
217 elif (e
.application_error
==
218 file_service_pb
.FileServiceErrors
.EXCLUSIVE_LOCK_FAILED
):
219 raise ExclusiveLockFailedError(e
)
223 def _create_rpc(deadline
):
224 """Create RPC object for file service.
227 deadling: Request deadline in seconds.
229 return apiproxy_stub_map
.UserRPC('file', deadline
)
232 def _make_call(method
, request
, response
,
234 """Perform File RPC call.
237 method: Service method name as string.
238 request: Request protocol buffer.
239 response: Response protocol buffer.
240 deadline: Request deadline in seconds.
243 Error or it's descendant if any File API specific error has happened.
246 rpc
= _create_rpc(deadline
=deadline
)
247 rpc
.make_call(method
, request
, response
)
251 except apiproxy_errors
.ApplicationError
, e
:
258 File object must be obtained by open() function and closed by its close()
259 method. It supports scoped closing by with operator.
262 def __init__(self
, filename
, mode
, content_type
, exclusive_lock
):
266 filename: File's name as string.
267 content_type: File's content type. Value from FileContentType.ContentType
270 self
._filename
= filename
272 self
._content
_type
= content_type
274 self
._exclusive
_lock
= exclusive_lock
278 def close(self
, finalize
=False):
282 finalize: Specifies if file should be finalized upon closing.
287 request
= file_service_pb
.CloseRequest()
288 response
= file_service_pb
.CloseResponse()
289 request
.set_filename(self
._filename
)
290 request
.set_finalize(finalize
)
291 self
._make
_rpc
_call
_with
_retry
('Close', request
, response
)
296 def __exit__(self
, atype
, value
, traceback
):
299 def write(self
, data
, sequence_key
=None):
300 """Write data to file.
303 data: Data to be written to the file. For RAW files it should be a string
305 sequence_key: Sequence key to use for write. Is used for RAW files only.
306 File API infrastructure ensures that sequence_key are monotonically
307 increasing. If sequence key less than previous one is used, a
308 SequenceKeyOutOfOrderError exception with last recorded sequence key
309 will be raised. If part of already written content is lost due to
310 infrastructure failure, last_sequence_key will point to last
311 successfully written key.
314 SequenceKeyOutOfOrderError: Raised when passed sequence keys are not
315 monotonically increasing.
316 InvalidArgumentError: Raised when wrong object type is apssed in as data.
317 Error: Error or its descendants are raised when other error has happened.
319 if self
._content
_type
== RAW
:
320 request
= file_service_pb
.AppendRequest()
321 response
= file_service_pb
.AppendResponse()
322 request
.set_filename(self
._filename
)
323 request
.set_data(data
)
325 request
.set_sequence_key(sequence_key
)
326 self
._make
_rpc
_call
_with
_retry
('Append', request
, response
)
328 raise UnsupportedContentTypeError(
329 'Unsupported content type: %s' % self
._content
_type
)
332 """Return file's current position.
334 Is valid only when file is opened for read.
336 self
._verify
_read
_mode
()
339 def seek(self
, offset
, whence
=os
.SEEK_SET
):
340 """Set the file's current position.
343 offset: seek offset as number.
344 whence: seek mode. Supported modes are os.SEEK_SET (absolute seek),
345 and os.SEEK_CUR (seek relative to the current position) and os.SEEK_END
346 (seek relative to the end, offset should be negative).
348 self
._verify
_read
_mode
()
349 if whence
== os
.SEEK_SET
:
350 self
._offset
= offset
351 elif whence
== os
.SEEK_CUR
:
352 self
._offset
+= offset
353 elif whence
== os
.SEEK_END
:
354 file_stat
= self
.stat()
355 self
._offset
= file_stat
.st_size
+ offset
357 raise InvalidArgumentError('Whence mode %d is not supported', whence
)
359 def read(self
, size
=None):
360 """Read data from RAW file.
363 size: Number of bytes to read as integer. Actual number of bytes
364 read might be less than specified, but it's never 0 unless current
365 offset is at the end of the file. If it is None, then file is read
369 A string with data read.
371 self
._verify
_read
_mode
()
372 if self
._content
_type
!= RAW
:
373 raise UnsupportedContentTypeError(
374 'Unsupported content type: %s' % self
._content
_type
)
376 buf
= StringIO
.StringIO()
377 original_offset
= self
._offset
384 request
= file_service_pb
.ReadRequest()
385 response
= file_service_pb
.ReadResponse()
386 request
.set_filename(self
._filename
)
387 request
.set_pos(self
._offset
)
388 request
.set_max_bytes(min(READ_BLOCK_SIZE
, size
))
389 self
._make
_rpc
_call
_with
_retry
('Read', request
, response
)
390 chunk
= response
.data()
391 self
._offset
+= len(chunk
)
397 return buf
.getvalue()
399 self
._offset
= original_offset
404 def _verify_read_mode(self
):
405 if self
._mode
not in ('r', 'rb'):
406 raise WrongOpenModeError('File is opened for write.')
409 request
= file_service_pb
.OpenRequest()
410 response
= file_service_pb
.OpenResponse()
412 request
.set_filename(self
._filename
)
413 request
.set_exclusive_lock(self
._exclusive
_lock
)
414 request
.set_content_type(self
._content
_type
)
416 if self
._mode
in ('a', 'ab'):
417 request
.set_open_mode(file_service_pb
.OpenRequest
.APPEND
)
418 elif self
._mode
in ('r', 'rb'):
419 request
.set_open_mode(file_service_pb
.OpenRequest
.READ
)
421 raise UnsupportedOpenModeError('Unsupported open mode: %s', self
._mode
)
423 self
._make
_rpc
_call
_with
_retry
('Open', request
, response
)
425 def _make_rpc_call_with_retry(self
, method
, request
, response
):
427 _make_call(method
, request
, response
)
428 except (ApiTemporaryUnavailableError
, FileTemporaryUnavailableError
):
431 _make_call(method
, request
, response
)
433 if self
._exclusive
_lock
:
438 _make_call(method
, request
, response
)
441 """Get status of a finalized file.
444 a _FileStat object similar to that returned by python's os.stat(path).
447 FinalizationError if file is not finalized.
449 self
._verify
_read
_mode
()
451 request
= file_service_pb
.StatRequest()
452 response
= file_service_pb
.StatResponse()
453 request
.set_filename(self
._filename
)
455 _make_call('Stat', request
, response
)
457 if response
.stat_size() == 0:
458 raise ExistenceError("File %s not found." % self
._filename
)
460 if response
.stat_size() > 1:
462 "Requested stat for one file. Got more than one response.")
464 file_stat_pb
= response
.stat(0)
465 file_stat
= _FileStat()
466 file_stat
.filename
= file_stat_pb
.filename()
467 file_stat
.finalized
= file_stat_pb
.finalized()
468 file_stat
.st_size
= file_stat_pb
.length()
469 file_stat
.st_mtime
= file_stat_pb
.mtime()
470 file_stat
.st_ctime
= file_stat_pb
.ctime()
478 exclusive_lock
=False,
483 filename: A name of the file as string.
484 mode: File open mode. Either 'a' or 'r'.
485 content_type: File's content type. Value from FileContentType.ContentType
487 exclusive_lock: If file should be exclusively locked. All other exclusive
488 lock attempts will file until file is correctly closed.
489 buffering: optional argument similar to the one in Python's open.
490 It specifies the file's desired buffer size: 0 means unbuffered, positive
491 value means use a buffer of that size, any negative value means the
492 default size. Only read buffering is supported.
498 InvalidArgumentError: Raised when given illegal argument value or type.
501 raise InvalidArgumentError('Filename is empty')
502 if not isinstance(filename
, basestring
):
503 raise InvalidArgumentError('Filename should be a string but is %s (%s)' %
504 (filename
.__class
__, filename
))
505 if content_type
!= RAW
:
506 raise InvalidArgumentError('Invalid content type')
507 if not (isinstance(buffering
, int) or isinstance(buffering
, long)):
508 raise InvalidArgumentError('buffering should be an int but is %s'
511 if mode
== 'r' or mode
== 'rb':
513 return BufferedFile(filename
, buffering
)
515 return BufferedFile(filename
, _DEFAULT_BUFFER_SIZE
)
517 return _File(filename
,
519 content_type
=content_type
,
520 exclusive_lock
=exclusive_lock
)
523 def listdir(path
, **kwargs
):
524 """Return a sorted list of filenames (matching a pattern) in the given path.
526 Only Google Cloud Storage paths are supported in current implementation.
529 path: a Google Cloud Storage path of "/gs/bucketname" form.
530 kwargs: other keyword arguments to be relayed to Google Cloud Storage.
531 This can be used to select certain files with names matching a pattern.
532 See google.appengine.api.files.gs.listdir for details.
535 a list containing filenames (matching a pattern) from the given path.
536 Sorted by Python String.
539 from google
.appengine
.api
.files
import gs
541 if not isinstance(path
, basestring
):
542 raise InvalidArgumentError('path should be a string, but is %s(%r)' %
543 (path
.__class
__.__name
__, path
))
545 if path
.startswith(gs
._GS
_PREFIX
):
546 return gs
.listdir(path
, kwargs
)
548 raise InvalidFileNameError('Unsupported path: %s' % path
)
551 def finalize(filename
, content_type
=RAW
):
555 filename: File name as string.
556 content_type: File's content type. Value from FileContentType.ContentType
560 raise InvalidArgumentError('Filename is empty')
561 if not isinstance(filename
, basestring
):
562 raise InvalidArgumentError('Filename should be a string')
563 if content_type
!= RAW
:
564 raise InvalidArgumentError('Invalid content type')
567 f
= open(filename
, 'a', exclusive_lock
=True, content_type
=content_type
)
568 f
.close(finalize
=True)
569 except FinalizationError
:
574 class _FileStat(object):
575 """_FileStat contains file attributes.
578 filename: the uploaded filename of the file;
579 finalized: whether the file is finalized. This is always true by now;
580 st_size: number of bytes of the file;
581 st_ctime: creation time;
582 st_mtime: modification time.
586 self
.finalized
= True
593 """Get status of a finalized file given it's full path filename.
596 a _FileStat object similar to that returned by python's os.stat(path).
599 FinalizationError if file is not finalized.
602 raise InvalidArgumentError('Filename is empty')
603 if not isinstance(filename
, basestring
):
604 raise InvalidArgumentError('Filename should be a string')
606 with
open(filename
, 'r') as f
:
610 def _create(filesystem
, content_type
=RAW
, filename
=None, params
=None):
614 filesystem: File system to create a file at as string.
615 content_type: File content type.
616 filename: Requested file name as string. Some file system require this
617 to be filled in, some require it to be None.
618 params: {string: string} dict of file parameters. Each filesystem
619 interprets them differently.
622 raise InvalidArgumentError('Filesystem is empty')
623 if not isinstance(filesystem
, basestring
):
624 raise InvalidArgumentError('Filesystem should be a string')
625 if content_type
!= RAW
:
626 raise InvalidArgumentError('Invalid content type')
628 request
= file_service_pb
.CreateRequest()
629 response
= file_service_pb
.CreateResponse()
631 request
.set_filesystem(filesystem
)
632 request
.set_content_type(content_type
)
635 if not isinstance(filename
, basestring
):
636 raise InvalidArgumentError('Filename should be a string')
637 request
.set_filename(filename
)
640 if not isinstance(params
, dict):
641 raise InvalidArgumentError('Parameters should be a dictionary')
642 for k
,v
in params
.items():
643 param
= request
.add_parameters()
647 _make_call('Create', request
, response
)
648 return response
.filename()
651 def __checkIsFinalizedName(filename
):
652 """Check if a filename is finalized.
654 A filename is finalized when it has creation handle prefix, which is the same
655 for both blobstore and gs files.
658 filename: a gs or blobstore filename that starts with '/gs/' or
662 InvalidFileNameError: raised when filename is finalized.
664 if filename
.split('/')[2].startswith(_CREATION_HANDLE_PREFIX
):
665 raise InvalidFileNameError('File %s should have finalized filename' %
669 def delete(*filenames
):
670 """Permanently delete files.
672 Delete on non-finalized/non-existent files is a no-op.
675 filenames: finalized file names as strings. filename should has format
676 "/gs/bucket/filename" or "/blobstore/blobkey".
679 InvalidFileNameError: Raised when any filename is not of valid format or
680 not a finalized name.
681 IOError: Raised if any problem occurs contacting the backend system.
684 from google
.appengine
.api
.files
import blobstore
as files_blobstore
685 from google
.appengine
.api
.files
import gs
686 from google
.appengine
.ext
import blobstore
690 for filename
in filenames
:
691 if not isinstance(filename
, basestring
):
692 raise InvalidArgumentError('Filename should be a string, but is %s(%r)' %
693 (filename
.__class
__.__name
__, filename
))
694 if filename
.startswith(files_blobstore
._BLOBSTORE
_DIRECTORY
):
695 __checkIsFinalizedName(filename
)
696 blobkey
= files_blobstore
.get_blob_key(filename
)
698 blobkeys
.append(blobkey
)
699 elif filename
.startswith(gs
._GS
_PREFIX
):
701 __checkIsFinalizedName(filename
)
702 blobkeys
.append(blobstore
.create_gs_key(filename
))
704 raise InvalidFileNameError('Filename should start with /%s or /%s' %
705 (files_blobstore
._BLOBSTORE
_DIRECTORY
,
709 blobstore
.delete(blobkeys
)
711 raise IOError('Blobstore failure.', e
)
714 def _get_capabilities():
715 """Get files API capabilities.
718 An instance of file_service_pb.GetCapabilitiesResponse.
720 request
= file_service_pb
.GetCapabilitiesRequest()
721 response
= file_service_pb
.GetCapabilitiesResponse()
723 _make_call('GetCapabilities', request
, response
)
727 class BufferedFile(object):
728 """BufferedFile is a file-like object reading underlying file in chunks."""
730 def __init__(self
, filename
, buffer_size
=_DEFAULT_BUFFER_SIZE
):
734 filename: the name of the file to read as string.
735 buffer_size: buffer read size to use as int.
737 self
._filename
= filename
741 self
._buffer
_size
= buffer_size
747 def __exit__(self
, atype
, value
, traceback
):
756 """Return file's current position."""
757 return self
._position
759 def read(self
, size
=None):
760 """Read data from RAW file.
763 size: Number of bytes to read as integer. Actual number of bytes
764 read is always equal to size unless end if file was reached.
767 A string with data read.
773 result
= self
.__readBuffer
(size
)
774 data_list
.append(result
)
776 if size
== 0 or self
._eof
:
777 return ''.join(data_list
)
778 self
.__refillBuffer
()
780 def readline(self
, size
=-1):
781 """Read one line delimited by '\n' from the file.
783 A trailing newline character is kept in the string. It may be absent when a
784 file ends with an incomplete line. If the size argument is non-negative,
785 it specifies the maximum string size (counting the newline) to return. An
786 empty string is returned only when EOF is encountered immediately.
789 size: Maximum number of bytes to read. If not specified, readline stops
793 The data read as a string.
800 end_pos
= len(self
._buffer
)
802 end_pos
= self
._buffer
_pos
+ size
803 newline_pos
= self
._buffer
.find('\n', self
._buffer
_pos
, end_pos
)
805 if newline_pos
!= -1:
807 data_list
.append(self
.__readBuffer
(newline_pos
+ 1 - self
._buffer
_pos
))
808 return ''.join(data_list
)
810 result
= self
.__readBuffer
(size
)
811 data_list
.append(result
)
813 if size
== 0 or self
._eof
:
814 return ''.join(data_list
)
815 self
.__refillBuffer
()
817 def __readBuffer(self
, size
):
818 """Read chars from self._buffer.
821 size: number of chars to read. Read the entire buffer if negative.
824 chars read in string.
827 size
= len(self
._buffer
) - self
._buffer
_pos
828 result
= self
._buffer
[self
._buffer
_pos
:self
._buffer
_pos
+size
]
830 self
._position
+= len(result
)
832 self
._buffer
_pos
+= len(result
)
835 def __refillBuffer(self
):
836 """Refill _buffer with another read from source."""
837 with
open(self
._filename
, 'r') as f
:
838 f
.seek(self
._position
)
839 data
= f
.read(self
._buffer
_size
)
840 self
._eof
= len(data
) < self
._buffer
_size
844 def seek(self
, offset
, whence
=os
.SEEK_SET
):
845 """Set the file's current position.
848 offset: seek offset as number.
849 whence: seek mode. Supported modes are os.SEEK_SET (absolute seek),
850 os.SEEK_CUR (seek relative to the current position), and os.SEEK_END
851 (seek relative to the end, offset should be negative).
853 if whence
== os
.SEEK_SET
:
854 self
._position
= offset
855 elif whence
== os
.SEEK_CUR
:
856 self
._position
+= offset
857 elif whence
== os
.SEEK_END
:
858 file_stat
= stat(self
._filename
)
859 self
._position
= file_stat
.st_size
+ offset
861 raise InvalidArgumentError('Whence mode %d is not supported', whence
)
867 def _default_gs_bucket_name():
868 """Return the default Google Storage bucket name for the application.
871 A string that is the default bucket name for the application.
873 request
= file_service_pb
.GetDefaultGsBucketNameRequest()
874 response
= file_service_pb
.GetDefaultGsBucketNameResponse()
876 _make_call('GetDefaultGsBucketName', request
, response
)
878 return response
.default_gs_bucket_name()