使用boto将公共URL上可用的图像上传到S3

59

我在Python的Web环境中工作,使用boto的key.set_contents_from_filename(path/to/file)可以简单地将文件从文件系统上传到S3。然而,我想上传一个已经存在于网络上的图像(比如 https://pbs.twimg.com/media/A9h_htACIAAaCf6.jpg:large)。

我是否应该以某种方式将图像下载到文件系统,然后像往常一样使用boto将其上传到S3,然后删除图像?

理想情况是,如果有一种方法可以让boto的key.set_contents_from_file或其他命令接受URL,并将图像流畅地传输到S3,而不必明确地下载文件副本到我的服务器。

def upload(url):
    try:
        conn = boto.connect_s3(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY)
        bucket_name = settings.AWS_STORAGE_BUCKET_NAME
        bucket = conn.get_bucket(bucket_name)
        k = Key(bucket)
        k.key = "test"
        k.set_contents_from_file(url)
        k.make_public()
                return "Success?"
    except Exception, e:
            return e
使用上述的set_contents_from_file方法,我遇到了“string object has no attribute 'tell'” 错误。使用set_contents_from_filename方法并提供url时,我遇到了“No such file or directory”错误。 boto存储文档 中只讲解了上传本地文件,并没有提到上传远程文件。

你只是想避免写入磁盘吗?还是完全避免将文件传输到您的计算机上? - Emily
理想情况下,可以将URL传递给S3,这样我的服务器就不必在磁盘上写入或加载内存。但我认为这并不是对S3服务的合理期望。如果必须由我的服务器处理,我宁愿不写入磁盘。 - dgh
10个回答

39

以下是我使用requests的方法,关键在于初始请求时设置stream=True,并使用upload.fileobj()方法将其上传到s3:

import requests
import boto3

url = "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"
r = requests.get(url, stream=True)

session = boto3.Session()
s3 = session.resource('s3')

bucket_name = 'your-bucket-name'
key = 'your-key-name' # key is the name of file on your bucket

bucket = s3.Bucket(bucket_name)
bucket.upload_fileobj(r.raw, key)

1
我正在学习boto并更加熟悉AWS。您能否用通俗易懂的语言告诉我为什么不能只做s3 = boto3.resource('s3')?默认会话不是已经开始了吗? - heartmo
4
@heartmo 这里的讨论提供了客户端、会话和资源之间差异的很好概述。https://dev59.com/PVgQ5IYBdhLWcg3wazMi - blaklaybul
完成了。非常感谢。 - Geshan Ravindu

23

好的,来自 @garnaat 的消息显示,目前似乎S3不允许通过URL上传。我成功地将远程图像仅读入内存后上传到了S3。这样可以实现上传。

def upload(url):
    try:
        conn = boto.connect_s3(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY)
        bucket_name = settings.AWS_STORAGE_BUCKET_NAME
        bucket = conn.get_bucket(bucket_name)
        k = Key(bucket)
        k.key = url.split('/')[::-1][0]    # In my situation, ids at the end are unique
        file_object = urllib2.urlopen(url)           # 'Like' a file object
        fp = StringIO.StringIO(file_object.read())   # Wrap object    
        k.set_contents_from_file(fp)
        return "Success"
    except Exception, e:
        return e

同时感谢《如何从urllib.urlopen()返回的“类文件对象”创建GzipFile实例?》


5
我不确定百分之百,但我认为 url.split('/')[::-1][0] 可以简单地改写为 url.split('/')[-1]。我的意思是,我无法想象出任何结果会有所不同的情况。 - Jordan Reiter

19

对于这个问题的2017年相关答案,使用官方的“boto3”包(而不是原始答案中的旧“boto”包):

Python 3.5

如果您在干净的Python安装上,请首先安装两个软件包:

pip install boto3

pip install requests

import boto3
import requests

# Uses the creds in ~/.aws/credentials
s3 = boto3.resource('s3')
bucket_name_to_upload_image_to = 'photos'
s3_image_filename = 'test_s3_image.png'
internet_image_url = 'https://docs.python.org/3.7/_static/py.png'


# Do this as a quick and easy check to make sure your S3 access is OK
for bucket in s3.buckets.all():
    if bucket.name == bucket_name_to_upload_image_to:
        print('Good to go. Found the bucket to upload the image into.')
        good_to_go = True

if not good_to_go:
    print('Not seeing your s3 bucket, might want to double check permissions in IAM')

# Given an Internet-accessible URL, download the image and upload it to S3,
# without needing to persist the image to disk locally
req_for_image = requests.get(internet_image_url, stream=True)
file_object_from_req = req_for_image.raw
req_data = file_object_from_req.read()

# Do the actual upload to s3
s3.Bucket(bucket_name_to_upload_image_to).put_object(Key=s3_image_filename, Body=req_data)

我使用上述方法时遇到了异常:S3上传异常:_send_request()需要5个位置参数,但提供了6个。 - ifti
1
@ifti 看起来你可能遇到了这个 bug - https://github.com/boto/botocore/issues/1079 现在看起来已经被修复了。 - GISD

11

很遗憾,目前没有方法可以实现这一点。我们可以在boto中添加一个方法,例如set_contents_from_url,但是该方法仍然必须将文件下载到本地计算机,然后上传它。这可能仍然是一个方便的方法,但不会为您节省任何东西。

要真正实现您想要的功能,我们需要在S3服务本身上具备一些功能,使我们能够将URL传递给它,并将其存储到桶中。这听起来像是一个非常有用的功能。您可能想要将其发布到S3论坛中。


谢谢,很高兴知道我没有错过一个可能有用的S3功能。我在论坛中记录了一个功能请求。 - dgh
1
可以通过使用stream=True来流式传输请求内容,并使用boto的upload_fileobj()方法上传文件。有关详细信息,请参见下面的答案。 - blaklaybul

8

一个简单的三行实现,可以直接在lambda上运行:

import boto3
import requests

s3_object = boto3.resource('s3').Object(bucket_name, object_key)

with requests.get(url, stream=True) as r:
    s3_object.put(Body=r.content)
.get 部分的源代码来自 requests 文档。

你尝试在哪些文件类型上进行了测试?当从s3打开我的jpg文件时,它们会损坏。 - NorwegianClassic

4
from io import BytesIO
def send_image_to_s3(url, name):
    print("sending image")
    bucket_name = 'XXX'
    AWS_SECRET_ACCESS_KEY = "XXX"
    AWS_ACCESS_KEY_ID = "XXX"

    s3 = boto3.client('s3', aws_access_key_id=AWS_ACCESS_KEY_ID,
                      aws_secret_access_key=AWS_SECRET_ACCESS_KEY)

    response = requests.get(url)
    img = BytesIO(response.content)

    file_name = f'path/{name}'
    print('sending {}'.format(file_name))
    r = s3.upload_fileobj(img, bucket_name, file_name)

    s3_path = 'path/' + name
    return s3_path

3

我用boto3尝试了以下操作,它对我有效:

import boto3;
import contextlib;
import requests;
from io import BytesIO;

s3 = boto3.resource('s3');
s3Client = boto3.client('s3')
for bucket in s3.buckets.all():
  print(bucket.name)


url = "@resource url";
with contextlib.closing(requests.get(url, stream=True, verify=False)) as response:
        # Set up file stream from response content.
        fp = BytesIO(response.content)
        # Upload data to S3
        s3Client.upload_fileobj(fp, 'aws-books', 'reviews_Electronics_5.json.gz')

2
使用boto3的upload_fileobj方法,您可以将文件流式传输到S3存储桶中,而无需保存到磁盘。以下是我的函数:
import boto3
import StringIO
import contextlib
import requests

def upload(url):
    # Get the service client
    s3 = boto3.client('s3')

    # Rember to se stream = True.
    with contextlib.closing(requests.get(url, stream=True, verify=False)) as response:
        # Set up file stream from response content.
        fp = StringIO.StringIO(response.content)
        # Upload data to S3
        s3.upload_fileobj(fp, 'my-bucket', 'my-dir/' + url.split('/')[-1])

2

目前看来,S3不支持远程上传。您可以使用以下类将图像上传到S3。此处的上传方法首先尝试下载图像,并在它被上传之前将其保存在内存中一段时间。要能够连接到S3,您需要使用命令pip install awscli安装AWS CLI,然后使用命令aws configure输入一些凭据:

import urllib3
import uuid
from pathlib import Path
from io import BytesIO
from errors import custom_exceptions as cex

BUCKET_NAME = "xxx.yyy.zzz"
POSTERS_BASE_PATH = "assets/wallcontent"
CLOUDFRONT_BASE_URL = "https://xxx.cloudfront.net/"


class S3(object):
    def __init__(self):
        self.client = boto3.client('s3')
        self.bucket_name = BUCKET_NAME
        self.posters_base_path = POSTERS_BASE_PATH

    def __download_image(self, url):
        manager = urllib3.PoolManager()
        try:
            res = manager.request('GET', url)
        except Exception:
            print("Could not download the image from URL: ", url)
            raise cex.ImageDownloadFailed
        return BytesIO(res.data)  # any file-like object that implements read()

    def upload_image(self, url):
        try:
            image_file = self.__download_image(url)
        except cex.ImageDownloadFailed:
            raise cex.ImageUploadFailed

        extension = Path(url).suffix
        id = uuid.uuid1().hex + extension
        final_path = self.posters_base_path + "/" + id
        try:
            self.client.upload_fileobj(image_file,
                                       self.bucket_name,
                                       final_path
                                       )
        except Exception:
            print("Image Upload Error for URL: ", url)
            raise cex.ImageUploadFailed

        return CLOUDFRONT_BASE_URL + id

1
import boto
from boto.s3.key import Key
from boto.s3.connection import OrdinaryCallingFormat
from urllib import urlopen


def upload_images_s3(img_url):
    try:
        connection = boto.connect_s3('access_key', 'secret_key', calling_format=OrdinaryCallingFormat())       
        bucket = connection.get_bucket('boto-demo-1519388451')
        file_obj = Key(bucket)
        file_obj.key = img_url.split('/')[::-1][0]
        fp = urlopen(img_url)
        result = file_obj.set_contents_from_string(fp.read())
    except Exception, e:
        return e

它真的会工作吗?尽管文件格式不同? - prhmma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接