diff --git a/aliyun/log/__init__.py b/aliyun/log/__init__.py index 5f4162e6..ff2691d7 100755 --- a/aliyun/log/__init__.py +++ b/aliyun/log/__init__.py @@ -48,7 +48,7 @@ from .metering_mode_response import GetLogStoreMeteringModeResponse, \ GetMetricStoreMeteringModeResponse, \ UpdateLogStoreMeteringModeResponse, UpdateMetricStoreMeteringModeResponse - +from .object_response import PutObjectResponse, GetObjectResponse from .store_view import StoreView, StoreViewStore from .store_view_response import CreateStoreViewResponse, UpdateStoreViewResponse, DeleteStoreViewResponse, ListStoreViewsResponse, GetStoreViewResponse diff --git a/aliyun/log/auth.py b/aliyun/log/auth.py index c640a26b..95149776 100644 --- a/aliyun/log/auth.py +++ b/aliyun/log/auth.py @@ -60,7 +60,7 @@ def _getGMT(): logger.warning("failed to set locale time to C. skip it: {0}".format(ex)) return datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') - def sign_request(self, method, resource, params, headers, body): + def sign_request(self, method, resource, params, headers, body, compute_content_hash=True): credentials = self.credentials_provider.get_credentials() if credentials.get_security_token(): headers['x-acs-security-token'] = credentials.get_security_token() @@ -68,7 +68,7 @@ def sign_request(self, method, resource, params, headers, body): headers['x-log-signaturemethod'] = 'hmac-sha1' headers['Date'] = self._getGMT() - if body: + if body and compute_content_hash: headers['Content-MD5'] = Util.cal_md5(body) if not credentials.get_access_key_secret(): return six.b('') @@ -92,7 +92,7 @@ def __init__(self, credentials_provider, region): AuthBase.__init__(self, credentials_provider) self._region = region - def sign_request(self, method, resource, params, headers, body): + def sign_request(self, method, resource, params, headers, body, compute_content_hash=True): current_time = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") headers['Authorization'] = self._do_sign_request(method, resource, params, headers, body, current_time) diff --git a/aliyun/log/logclient.py b/aliyun/log/logclient.py index bf954f89..84bf9fa3 100644 --- a/aliyun/log/logclient.py +++ b/aliyun/log/logclient.py @@ -10,9 +10,9 @@ import requests import six import time -import zlib from datetime import datetime import logging +import re from .store_view_response import ListStoreViewsResponse, CreateStoreViewResponse, UpdateStoreViewResponse, DeleteStoreViewResponse, GetStoreViewResponse from .credentials import StaticCredentialsProvider @@ -76,7 +76,8 @@ from .metering_mode_response import GetLogStoreMeteringModeResponse, \ GetMetricStoreMeteringModeResponse, UpdateLogStoreMeteringModeResponse, \ UpdateMetricStoreMeteringModeResponse -from .util import require_python3 +from .object_response import PutObjectResponse, GetObjectResponse +from .util import require_python3, object_name_encode logger = logging.getLogger(__name__) @@ -310,7 +311,7 @@ def _sendRequest(self, method, url, params, body, headers, respons_body_type='js 'Request is failed. Http code is ' + str(resp_status) + exJson, requestId, resp_status, resp_header, resp_body) - def _send(self, method, project, body, resource, params, headers, respons_body_type='json'): + def _send(self, method, project, body, resource, params, headers, respons_body_type='json', compute_content_hash=True): if body: headers['Content-Length'] = str(len(body)) else: @@ -337,7 +338,7 @@ def _send(self, method, project, body, resource, params, headers, respons_body_t params2 = copy(params) if self._securityToken: headers2["x-acs-security-token"] = self._securityToken - self._auth.sign_request(method, resource, params2, headers2, body) + self._auth.sign_request(method, resource, params2, headers2, body, compute_content_hash=compute_content_hash) return self._sendRequest(method, url, params2, body, headers2, respons_body_type) except LogException as ex: last_err = ex @@ -6511,3 +6512,77 @@ def delete_store_view(self, project_name, store_view_name): resource = "/storeviews/" + store_view_name (resp, header) = self._send("DELETE", project_name, None, resource, params, {}) return DeleteStoreViewResponse(header, resp) + + def put_object( + self, project_name, logstore_name, object_name, content, headers=None + ): + """Put an object to the specified logstore. + Unsuccessful operation will cause an LogException. + + :type project_name: string + :param project_name: the project name + + :type logstore_name: string + :param logstore_name: the logstore name + + :type object_name: string + :param object_name: the object name (only allow a-z A-Z 0-9 _ -) + + :type content: bytes/string + :param content: the object content (can be empty) + + :type headers: dict + :param headers: optional headers to send with the request. + - x-log-meta-* headers will be attached to the object as metadata, and returned in the response when getting the object + - Content-Type will be attached to the object as metadata, and returned in the response when getting the object + - Content-MD5 will be attached to the object as metadata, and returned in the response when getting the object + + :return: PutObjectResponse + + :raise: LogException + """ + encoded_object_name = object_name_encode(object_name) + if headers is None: + headers = {} + else: + headers = dict(headers) + + if isinstance(content, six.text_type): + body = content.encode("utf-8") + elif isinstance(content, six.binary_type): + body = content + else: + raise LogException("InvalidParameter", "content must be bytes or string") + + headers["x-log-bodyrawsize"] = str(len(body)) + resource = "/logstores/" + logstore_name + "/objects/" + encoded_object_name + + (resp, resp_header) = self._send( + "PUT", project_name, body, resource, {}, headers, compute_content_hash=False + ) + return PutObjectResponse(resp_header, resp) + + def get_object(self, project_name, logstore_name, object_name): + """Get an object from the specified logstore. + Unsuccessful operation will cause an LogException. + + :type project_name: string + :param project_name: the project name + + :type logstore_name: string + :param logstore_name: the logstore name + + :type object_name: string + :param object_name: the object name + + :return: GetObjectResponse + + :raise: LogException + """ + encoded_object_name = object_name_encode(object_name) + resource = "/logstores/" + logstore_name + "/objects/" + encoded_object_name + + (resp, resp_header) = self._send( + "GET", project_name, None, resource, {}, {}, respons_body_type="raw" + ) + return GetObjectResponse(resp_header, resp) diff --git a/aliyun/log/object_response.py b/aliyun/log/object_response.py new file mode 100644 index 00000000..731385d5 --- /dev/null +++ b/aliyun/log/object_response.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logresponse import LogResponse +from .util import Util + +__all__ = ['PutObjectResponse', 'GetObjectResponse'] + + +class PutObjectResponse(LogResponse): + """ The response of the put_object API from log. + + :type header: dict + :param header: PutObjectResponse HTTP response header + + :type resp: string + :param resp: PutObjectResponse HTTP response body + """ + + def __init__(self, header, resp=''): + LogResponse.__init__(self, header, resp) + + def get_etag(self): + """ Get the ETag of the uploaded object. + + :return: string, ETag value + """ + return Util.h_v_td(self.headers, 'ETag', None) + + def log_print(self): + print('PutObjectResponse:') + print('headers:', self.get_all_headers()) + + +class GetObjectResponse(LogResponse): + """ The response of the get_object API from log. + + :type header: dict + :param header: GetObjectResponse HTTP response header + + :type resp: bytes + :param resp: GetObjectResponse HTTP response body (object content) + """ + + def __init__(self, header, resp): + LogResponse.__init__(self, header, resp) + + def get_etag(self): + """ Get the ETag of the object. + + :return: string, ETag value, may be None if not set + """ + return Util.h_v_td(self.headers, 'ETag', None) + + def get_last_modified(self): + """ Get the last modified time of the object. + + :return: string, last modified time, may be None if not set + """ + return Util.h_v_td(self.headers, 'Last-Modified', None) + + def get_content_type(self): + """ Get the content type of the object. + + :return: string, content type + """ + return Util.h_v_td(self.headers, 'Content-Type', '') + + def get_headers(self): + """ Get all headers of the response. + + :return: dict, all response headers + """ + return self.headers + + def get_body(self): + """ Get the object content. + + :return: bytes, object content + """ + return self.body + + def log_print(self): + print('GetObjectResponse:') + print('headers:', self.get_all_headers()) + diff --git a/aliyun/log/util.py b/aliyun/log/util.py index 35ea9377..194d764e 100755 --- a/aliyun/log/util.py +++ b/aliyun/log/util.py @@ -322,4 +322,14 @@ def wrapper(*args, **kwargs): if not six.PY3: raise RuntimeError("Function '{func_name}' requires Python 3 to run.".format(func_name=func.__name__)) return func(*args, **kwargs) - return wrapper \ No newline at end of file + return wrapper + + +if six.PY2: + from urllib import quote as urlquote +else: + from urllib.parse import quote as urlquote + urlquote + +def object_name_encode(object_name): + return urlquote(object_name) \ No newline at end of file diff --git a/aliyun/log/version.py b/aliyun/log/version.py index cd1d2de0..a22c346a 100644 --- a/aliyun/log/version.py +++ b/aliyun/log/version.py @@ -1,4 +1,4 @@ -__version__ = '0.9.37' +__version__ = '0.9.38' import sys OS_VERSION = str(sys.platform) diff --git a/doc/tutorials/object_api.md b/doc/tutorials/object_api.md new file mode 100644 index 00000000..809b83af --- /dev/null +++ b/doc/tutorials/object_api.md @@ -0,0 +1,179 @@ +# Object API 使用指南 + +## 概述 + +Object API 允许你在 SLS LogStore 中存储和检索任意对象数据。 + +## API 方法 + +### put_object + +上传一个对象到指定的 LogStore,支持携带自定义 header 元数据。 + +**方法签名:** + +```python +def put_object(self, project_name, logstore_name, object_name, content, headers=None): + """ + :type project_name: string + :param project_name: 项目名称 + + :type logstore_name: string + :param logstore_name: LogStore 名称 + + :type object_name: string + :param object_name: 对象名称(只允许 a-z A-Z 0-9 _ -) + + :type content: bytes/string + :param content: 对象内容(可以为空) + + :type headers: dict + :param headers: 可选的请求头(所有 headers 都会透传给服务端) + - x-log-meta-* 开头的 header 是对象的元数据,会在 get_object 时返回 + - Content-Type 请求类型 + - Content-MD5 MD5 值 + + :return: PutObjectResponse + + :raise: LogException + """ +``` + +**示例:** + +```python +from aliyun.log import LogClient + +# 初始化客户端 +client = LogClient(endpoint, access_key_id, access_key) + +# 示例 1: 上传简单文本对象 +object_name = 'my_object' +content = b'Hello, World!' +response = client.put_object(project, logstore, object_name, content) +print('ETag:', response.get_etag()) + +# 示例 2: 上传带元数据的对象 +headers = { + 'Content-Type': 'text/plain', + 'x-log-meta-author': 'user123', + 'x-log-meta-version': '1.0' +} +response = client.put_object(project, logstore, object_name, content, headers) + +# 示例 3: 上传带 Content-MD5 的对象 +import hashlib +import base64 + +md5_hash = hashlib.md5(content).digest() +content_md5 = base64.b64encode(md5_hash).decode('utf-8') + +headers = { + 'Content-MD5': content_md5, + 'Content-Type': 'application/octet-stream' +} +response = client.put_object(project, logstore, object_name, content, headers) +``` + +**HTTP 请求** + +```http +PUT /logstores/{logstore}/objects/{key} +Host: {project}.{endpoint} +Content-Type: image/jpg +Content-Length: 344606 +x-log-meta-a: a +x-log-meta-b: b + +<对象字节流> +``` + +**HTTP 响应** + +```http +HTTP/1.1 200 OK +x-log-request-id: 68FA41E1543C150DB612D36B +Date: Fri, 24 Feb 2012 06:38:30 GMT +Last-Modified: Fri, 24 Feb 2012 06:07:48 GMT +ETag: "5B3C1A2E0563E1B002CC607C*****" +Content-Type: image/jpg +Content-Length: 344606 +Server: AliyunSLS +``` + +### get_object + +从指定的 LogStore 获取一个对象。 + +**方法签名:** + +```python +def get_object(self, project_name, logstore_name, object_name): + """ + :type project_name: string + :param project_name: 项目名称 + + :type logstore_name: string + :param logstore_name: LogStore 名称 + + :type object_name: string + :param object_name: 对象名称 + + :return: GetObjectResponse + + :raise: LogException + """ +``` + +**示例:** + +```python +from aliyun.log import LogClient + +# 初始化客户端 +client = LogClient(endpoint, access_key_id, access_key) + +# 获取对象 +object_name = 'my_object' +response = client.get_object(project, logstore, object_name) + +# 访问响应数据 +print('ETag:', response.get_etag()) +print('Last Modified:', response.get_last_modified()) +print('Content Type:', response.get_content_type()) +print('Content Length:', len(response.get_body())) +print('Content:', response.get_body()) + +# 获取所有响应头 +headers = response.get_headers() +for key, value in headers.items(): + print('{}: {}'.format(key, value)) + +``` + +**HTTP 请求** + +```http +GET /logstores/{logstore}/objects/{key} +Host: {project}.{endpoint} +``` + +**HTTP 响应** + +```http +HTTP/1.1 200 OK +x-log-request-id: 68FA41E1543C150DB612D36B +Date: Fri, 24 Feb 2012 06:38:30 GMT +Last-Modified: Fri, 24 Feb 2012 06:07:48 GMT +ETag: "5B3C1A2E0563E1B002CC607C*****" +Content-Type: image/jpg +Content-Length: 344606 +x-log-meta-a: a +x-log-meta-b: b + +[344606 bytes of object data] +``` + +## 完整示例 + +参见 [sample_object_api](../../tests/sample_object_api.py) 示例程序。 diff --git a/tests/sample_object_api.py b/tests/sample_object_api.py new file mode 100644 index 00000000..d2b6b1c3 --- /dev/null +++ b/tests/sample_object_api.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +""" +Sample code to demonstrate Object API usage. + +This example shows how to use put_object and get_object methods. +""" + +import os +import sys + +from aliyun.log import LogClient +from aliyun.log.logexception import LogException + +endpoint = "cn-hangzhou.log.aliyuncs.com" # Replace with your endpoint +accessKeyId = os.getenv("ACCESS_KEY_ID") # Replace with your access key id +accessKey = os.getenv("ACCESS_KEY") # Replace with your access key +project = "" # Replace with your project name +logstore = "" # Replace with your logstore name + +client = LogClient(endpoint, accessKeyId, accessKey) + + +# Example 1: Put a simple text object +def sample_put_object(): + """ + Sample: Put an object to logstore + """ + try: + object_name = "test_object_1" + content = b"Hello, this is test content" + + response = client.put_object(project, logstore, object_name, content) + print("Put object success!") + response.log_print() + print('etag', response.get_etag()) + + response = client.get_object(project, logstore, object_name) + response.log_print() + print(response.get_body()) + print('etag', response.get_etag()) + print('last_modified', response.get_last_modified()) + print('content-type', response.get_content_type()) + + except LogException as e: + print("Put object failed:", e) + raise + + +# Example 2: Put an object with custom headers +def sample_put_with_header(): + try: + object_name = "test_object_2" + content = b"Content with metadata" + headers = { + "Content-Type": "text/plain", + "x-log-meta-author": "test_user", + "x-log-meta-version": "1.0", + } + + response = client.put_object(project, logstore, object_name, content, headers) + response.log_print() + print('etag', response.get_etag()) + print("Put object with headers success!") + + response = client.get_object(project, logstore, object_name) + response.log_print() + print(response.get_body()) + print('etag', response.get_etag()) + print('last_modified', response.get_last_modified()) + print('content-type', response.get_content_type()) + except LogException as e: + print("Put object failed:", e) + raise + + +# Example 3: Put an object with Content-MD5 +def sample_put_with_md5(): + try: + import hashlib + import base64 + + object_name = "test_object_3" + content = b"Content with MD5" + + # Calculate MD5 + md5_hash = hashlib.md5(content).digest() + content_md5 = base64.b64encode(md5_hash).decode("utf-8") + + headers = { + "Content-MD5": content_md5, + "Content-Type": "application/octet-stream", + } + + response = client.put_object(project, logstore, object_name, content, headers) + print("Put object with MD5 success!") + response.log_print() + print('etag', response.get_etag()) + + response = client.get_object(project, logstore, object_name) + response.log_print() + print(response.get_body()) + print('etag', response.get_etag()) + print('last_modified', response.get_last_modified()) + print('content-type', response.get_content_type()) + except LogException as e: + print("Put object failed:", e) + raise + + +if __name__ == "__main__": + client.list_project() + print("=" * 60) + print("Sample: Put Object") + print("=" * 60) + sample_put_object() + + print("\n" + "=" * 60) + print("Sample: Put Object with Custom Headers") + print("=" * 60) + sample_put_with_header() + + print("\n" + "=" * 60) + print("Sample: Put Object with Content-MD5") + print("=" * 60) + sample_put_with_md5()