# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

module AWS
  class S3

    # Represents a bucket in S3.
    #
    # = Creating Buckets
    #
    # You create a bucket by name.  Bucket names must be globally unique
    # and must be DNS compatible.
    #
    #   s3 = AWS::S3.new
    #   bucket = s3.buckets.create('dns-compat-bucket-name')
    #
    # = Getting a Bucket
    #
    # You can create a reference to a bucket, given its name.
    #
    #   bucket = s3.buckets['bucket-name'] # makes no request
    #   bucket.exists? #=> returns true/false
    #
    # = Enumerating Buckets
    #
    # The {BucketCollection} class is enumerable.
    #
    #   s3.buckets.each do |bucket|
    #     puts bucket.name
    #   end
    #
    # = Deleting a Bucket
    #
    # You can delete an empty bucket you own.
    #
    #   bucket = s3.buckets.create('my-temp-bucket')
    #   bucket.objects['abc'].write('xyz')
    #
    #   bucket.clear! # deletes all object versions in batches
    #   bucket.delete
    #
    # You can alternatively call {#delete!} which will clear
    # the bucket for your first.
    #
    #   bucket.delete!
    #
    # = Objects
    #
    # Given a bucket you can access its objects, either by key or by
    # enumeration.
    #
    #   bucket.objects['key'] #=> makes no request, returns an S3Object
    #
    #   bucket.objects.each do |obj|
    #     puts obj.key
    #   end
    #
    # See {ObjectCollection} and {S3Object} for more information on working
    # with objects.
    #
    # = Bucket Policies and ACLs
    #
    # You can control access to your bucket and its contents a number
    # of ways.  You can specify a bucket ACL (access control list)
    # or a bucket policy.
    #
    # == ACLs
    #
    # ACLs control access to your bucket and its contents via a list of
    # grants and grantees.
    #
    # === Canned ACLs
    #
    # The simplest way to specify an ACL is to use one of Amazon's "canned"
    # ACLs.  Amazon accepts the following canned ACLs:
    #
    # * +:private+
    # * +:public_read+
    # * +:public_read_write+
    # * +:authenticated_read+
    # * +:bucket_owner_read+
    # * +:bucket_owner_full_control+
    #
    # You can specify a the ACL at bucket creation or later update a bucket.
    #
    #   # at create time, defaults to :private when not specified
    #   bucket = s3.buckets.create('name', :acl => :public_read)
    #
    #   # replacing an existing bucket ACL
    #   bucket.acl = :private
    #
    # === Grants
    #
    # Alternatively you can specify a hash of grants.  Each entry in the
    # +:grant+ hash has a grant (key) and a list of grantees (values).
    # Valid grant keys are:
    #
    # * +:grant_read+
    # * +:grant_write+
    # * +:grant_read_acp+
    # * +:grant_write_acp+
    # * +:grant_full_control+
    #
    # Each grantee can be a String, Hash or array of strings or hashes.
    # The following example uses grants to provide public read
    # to everyone while providing full control to a user by email address
    # and to another by their account id (cannonical user id).
    #
    #   bucket = s3.buckets.create('name', :grants => {
    #     :grant_read => [
    #       { :uri => "http://acs.amazonaws.com/groups/global/AllUsers" },
    #     ],
    #     :grant_full_control => [
    #       { :id => 'abc...mno' }               # cannonical user id
    #       { :email_address => 'foo@bar.com' }, # email address
    #     ]
    #   })
    #
    # === ACL Object
    #
    # Lastly, you can build an ACL object and use a Ruby DSL to specify grants
    # and grantees.  See {ACLObject} for more information.
    #
    #   # updating an existing bucket acl using ACLObject
    #   bucket.acl.change do |acl|
    #     acl.grants.reject! do |g|
    #       g.grantee.canonical_user_id != bucket.owner.id
    #     end
    #   end
    #
    # == Policies
    #
    # You can also work with bucket policies.
    #
    #   policy = AWS::S3::Policy.new
    #   policy.allow(
    #     :actions => [:put_object, :get_object]
    #     :resources => [bucket]
    #     :principals => :any)
    #
    #   bucket.policy = policy
    #
    # See {Core::Policy} and {S3::Policy} for more information on build
    # policy objects.
    #
    # = Versioned Buckets
    #
    # You can enable versioning on a bucket you control.  When versioning
    # is enabled, S3 will keep track of each version of each object you
    # write to the bucket (even deletions).
    #
    #   bucket.versioning_enabled? #=> false
    #   bucket.enable_versioning
    #   # there is also a #disable_versioning method
    #
    #   obj = bucket.objects['my-obj']
    #   obj.write('a')
    #   obj.write('b')
    #   obj.delete
    #   obj.write('c')
    #
    #   obj.versions.each do |obj_version|
    #     if obj_version.delete_marker?
    #       puts obj_version.read
    #     else
    #       puts "- DELETE MARKER"
    #     end
    #   end
    #
    # Alternatively you can enumerate all versions of all objects in your
    # bucket.
    #
    #   bucket.versions.each do |obj_version|
    #     puts obj_version.key + " : " + obj_version.version_id
    #   end
    #
    # See {BucketVersionCollection}, {ObjectVersionCollection} and
    # {ObjectVersion} for more information on working with objects in
    # a versioned bucket.  Also see the S3 documentation for information
    # on object versioning.
    #
    class Bucket

      include Core::Model
      include ACLOptions

      # @param [String] name
      # @param [Hash] options
      # @option options [String] :owner (nil) The owner id of this bucket.
      def initialize(name, options = {})
        # the S3 docs disagree with what the service allows,
        # so it's not safe to toss out invalid bucket names
        # S3::Client.validate_bucket_name!(name)
        @name = name
        @owner = options[:owner]
        super
      end

      # @return [String] The bucket name
      attr_reader :name

      # Returns the url for this bucket.
      # @return [String] url to the bucket
      def url
        if client.dns_compatible_bucket_name?(name)
          "http://#{name}.s3.amazonaws.com/"
        else
          "http://s3.amazonaws.com/#{name}/"
        end
      end

      # @return [Boolean] Returns true if the bucket has no objects
      #   (this includes versioned objects that are delete markers).
      def empty?
        versions.first ? false : true
      end

      # @return [String,nil] Returns the location constraint for a bucket
      #   (if it has one), nil otherwise.
      def location_constraint
        client.get_bucket_location(:bucket_name => name).location_constraint
      end

      # Returns the tags for this bucket.
      #
      #   tags = bucket.tags
      #   #=> <AWS::S3::BucketTagCollection>
      #
      #   # adds a tag to the bucket
      #   tags['foo'] = 'abc'
      #
      #   # replaces all tags
      #   tags.set('new' => 'tags')
      #
      #   # removes all tags from the bucket
      #   tags.clear
      #
      #   # returns tags as a hash
      #   tags.to_h
      #
      # @return [BucketTagCollection] Returns a collection that represents
      #   the tags for this bucket.
      #
      def tags
        BucketTagCollection.new(self)
      end

      # Sets the tags for this bucket.
      #
      #   bucket.tags = { 'contents' => 'photots' }
      #
      # You can remove all tags for the bucket by passing an empty
      # hash or +nil+.
      #
      #   bucket.tags = nil # {} also deletes all tags
      #   bucket.tags
      #   #=> {}
      #
      # @param [Hash,nil] tags The tags to set on this bucket.
      #
      def tags= tags
        self.tags.set(tags)
      end

      # @return [CORSRuleCollection] Returns a collection that can be
      #   used to manage (add, edit and delete) CORS rules for this bucket.
      def cors
        CORSRuleCollection.new(self)
      end

      # Sets the bucket CORS rules.
      # @param (see CORSRuleCollection#set)
      # @see CORSRuleCollection#set
      def cors= *rules
        self.cors.set(*rules)
      end

      # Enables versioning on this bucket.
      #
      # @option opts [String] :mfa_delete Set to 'Enabled' or 'Disabled'
      #   to control the state of MFA delete on the bucket versioning.
      #   Setting this option requires the :mfa option to also be set.
      #
      # @option opts [String] :mfa The serial number and current token code of
      #   the Multi-Factor Authentication (MFA) device for the user. Format
      #   is "SERIAL TOKEN" - with a space between the serial and token.
      #
      # @return [nil]
      def enable_versioning(opts = {})
        client.set_bucket_versioning(
          :bucket_name => @name,
          :state       => :enabled,
          :mfa_delete  => opts[:mfa_delete],
          :mfa         => opts[:mfa])
        nil
      end

      # Suspends versioning on this bucket.
      #
      # @option opts [String] :mfa_delete Set to 'Enabled' or 'Disabled'
      #   to control the state of MFA delete on the bucket versioning.
      #   Setting this option requires the :mfa option to also be set.
      #
      # @option opts [String] :mfa The serial number and current token code of
      #   the Multi-Factor Authentication (MFA) device for the user. Format
      #   is "SERIAL TOKEN" - with a space between the serial and token.
      #
      # @return [nil]
      def suspend_versioning(opts = {})
        client.set_bucket_versioning(
          :bucket_name => @name,
          :state       => :suspended,
          :mfa_delete  => opts[:mfa_delete],
          :mfa         => opts[:mfa])
        nil
      end

      # @return [Boolean] returns +true+ if version is enabled on this bucket.
      def versioning_enabled?
        versioning_state == :enabled
      end
      alias_method :versioned?, :versioning_enabled?

      # Returns the versioning status for this bucket.  States include:
      #
      # * +:enabled+ - currently enabled
      # * +:suspended+ - currently suspended
      # * +:unversioned+ - versioning has never been enabled
      #
      # @return [Symbol] the versioning state
      def versioning_state
        client.get_bucket_versioning(:bucket_name => @name).status
      end

      # Deletes all objects from this bucket.
      # @return [nil]
      def clear!
        versions.each_batch do |versions|
          objects.delete(versions)
        end
      end

      # Deletes the current bucket.  An error will be raised if the
      # bucket is not empty.
      # @return [nil]
      def delete
        client.delete_bucket(:bucket_name => @name)
        nil
      end

      # Deletes all objects in a bucket and then deletes the bucket.
      # @return [nil]
      def delete!
        clear!
        delete
      end

      # @return [String] bucket owner id
      def owner
        @owner || client.list_buckets.owner
      end

      # @private
      def inspect
        "#<AWS::S3::Bucket:#{name}>"
      end

      # @return [Boolean] Returns true if the two buckets have the same name.
      def ==(other)
        other.kind_of?(Bucket) && other.name == name
      end

      # @return [Boolean] Returns true if the two buckets have the same name
      def eql?(other_bucket)
        self == other_bucket
      end

      # @note This method only indicates if there is a bucket in S3, not
      #   if you have permissions to work with the bucket or not.
      # @return [Boolean] Returns true if the bucket exists in S3.
      def exists?
        begin
          versioned? # makes a get bucket request without listing contents
                     # raises a client error if the bucket doesn't exist or
                     # if you don't have permission to get the bucket
                     # versioning status.
          true
        rescue Errors::NoSuchBucket => e
          false # bucket does not exist
        rescue Errors::ClientError => e
          true # bucket exists
        end
      end

      # @return [ObjectCollection] Represents all objects(keys) in
      #   this bucket.
      def objects
        ObjectCollection.new(self)
      end

      # @return [BucketVersionCollection] Represents all of the versioned
      #   objects stored in this bucket.
      def versions
        BucketVersionCollection.new(self)
      end

      # @return [MultipartUploadCollection] Represents all of the
      #   multipart uploads that are in progress for this bucket.
      def multipart_uploads
        MultipartUploadCollection.new(self)
      end

      # @private
      module ACLProxy

        attr_accessor :bucket

        def change
          yield(self)
          bucket.acl = self
        end

      end

      # Returns the bucket's access control list.  This will be an
      # instance of AccessControlList, plus an additional +change+
      # method:
      #
      #   bucket.acl.change do |acl|
      #     acl.grants.reject! do |g|
      #       g.grantee.canonical_user_id != bucket.owner.id
      #     end
      #   end
      #
      # @return [AccessControlList]
      def acl

        resp = client.get_bucket_acl(:bucket_name => name)

        acl = AccessControlList.new(resp.data)
        acl.extend ACLProxy
        acl.bucket = self
        acl

      end

      # Sets the bucket's ACL (access control list).  You can provide an ACL
      # in a number of different formats.
      # @param (see ACLOptions#acl_options)
      # @return [nil]
      def acl= acl
        client.set_bucket_acl(acl_options(acl).merge(:bucket_name => name))
        nil
      end

      # @private
      module PolicyProxy

        attr_accessor :bucket

        def change
          yield(self)
          bucket.policy = self
        end

        def delete
          bucket.client.delete_bucket_policy(:bucket_name => bucket.name)
        end

      end

      # Returns the bucket policy.  This will be an instance of
      # Policy.  The returned policy will also have the methods of
      # PolicyProxy mixed in, so you can use it to change the
      # current policy or delete it, for example:
      #
      #  if policy = bucket.policy
      #    # add a statement
      #    policy.change do |p|
      #      p.allow(...)
      #    end
      #
      #    # delete the policy
      #    policy.delete
      #  end
      #
      # Note that changing the policy is not an atomic operation; it
      # fetches the current policy, yields it to the block, and then
      # sets it again.  Therefore, it's possible that you may
      # overwrite a concurrent update to the policy using this
      # method.
      #
      # @return [Policy,nil] Returns the bucket policy (if it has one),
      #   or it returns +nil+ otherwise.
      def policy
        resp = client.get_bucket_policy(:bucket_name => name)
        policy = Policy.from_json(resp.data[:policy])
        policy.extend(PolicyProxy)
        policy.bucket = self
        policy
      rescue Errors::NoSuchBucketPolicy => e
        nil
      end

      # Sets the bucket's policy.
      #
      # @param policy The new policy.  This can be a string (which
      #   is assumed to contain a valid policy expressed in JSON), a
      #   Policy object or any object that responds to +to_json+.
      # @see Policy
      # @return [nil]
      def policy=(policy)
        client.set_bucket_policy(:bucket_name => name, :policy => policy)
        nil
      end

      # The primary interface for editing the lifecycle configuration.
      # See {BucketLifecycleConfiguration} for more information.
      #
      # @example Adding rules to a bucket's lifecycle configuration
      #
      #   bucket.lifecycle_configuration.update do
      #     add_rule 'cache-1/', 30
      #     add_rule 'cache-2/', 30
      #   end
      #
      # @example Deleting the lifecycle configuration
      #
      #   bucket.lifecycle_configuration.clear
      #
      # @return [BucketLifecycleConfiguration]
      #
      def lifecycle_configuration
        @lifecycle_cfg ||= BucketLifecycleConfiguration.new(self)
      end

      # You can call this method if you prefer to build your own
      # lifecycle configuration.
      #
      #   bucket.lifecycle_configuration = <<-XML
      #     <LifecycleConfiguration>
      #       ...
      #     </LifecycleConfiguration>
      #   XML
      #
      # You can also use this method to copy a lifecycle configuration
      # from another bucket.
      #
      #   bucket.lifecycle_configuration = other_bucket.lifecycle_configuration
      #
      # If you call this method, passing nil, the lifecycle configuration
      # for this bucket will be deleted.
      #
      # @param [String,Object] config You can pass an xml string or any
      #   other object that responds to #to_xml (e.g.
      #   BucketLifecycleConfiguration).
      #
      # @return [nil]
      #
      def lifecycle_configuration= config

        if config.nil?

          client_opts = {}
          client_opts[:bucket_name] = name
          client.delete_bucket_lifecycle_configuration(client_opts)

          @lifecycle_cfg = BucketLifecycleConfiguration.new(self, :empty => true)

        else

          xml = config.is_a?(String) ? config : config.to_xml

          client_opts = {}
          client_opts[:bucket_name] = name
          client_opts[:lifecycle_configuration] = xml
          client.set_bucket_lifecycle_configuration(client_opts)

          @lifecycle_cfg = BucketLifecycleConfiguration.new(self, :xml => xml)

        end

        nil

      end

      # Returns a tree that allows you to expose the bucket contents
      # like a directory structure.
      #
      # @see Tree
      # @param [Hash] options
      # @option options [String] :prefix (nil) Set prefix to choose where
      #   the top of the tree will be.  A value of +nil+ means
      #   that the tree will include all objects in the collection.
      #
      # @option options [String] :delimiter ('/') The string that separates
      #   each level of the tree.  This is usually a directory separator.
      #
      # @option options [Boolean] :append (true) If true, the delimiter is
      #   appended to the prefix when the prefix does not already end
      #   with the delimiter.
      #
      # @return [Tree]
      def as_tree options = {}
        objects.as_tree(options)
      end

      # Generates fields for a presigned POST to this object.  All
      # options are sent to the PresignedPost constructor.
      #
      # @see PresignedPost
      def presigned_post(options = {})
        PresignedPost.new(self, options)
      end

    end

  end
end