Skip to content

Commit

Permalink
Release v0.0.1 (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
misaelnieto authored Nov 17, 2023
1 parent 78f57bf commit 71ce949
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 55 deletions.
11 changes: 11 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]

[dev-packages]

[requires]
python_version = "3.9"
20 changes: 20 additions & 0 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 96 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,127 @@ export BLOB_READ_WRITE_TOKEN="vercel_blob_rw_ABC1234XYz"
### Put operation
```sh
vercel_storage blob upload path/to/file.zip
$ vercel_blob put disk_dump.bin
------------------ ----------------------------------------------------------------------------------------------------
url https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/disk_dump-OTgBsduT0QQcfXImNMZky1NSy3HfML.bin
pathname disk_dump.bin
contentType application/octet-stream
contentDisposition attachment; filename="disk_dump.bin"
------------------ ----------------------------------------------------------------------------------------------------
```
You can also print the output information as a json:
```sh
vercel_storage --json blob upload path/to/file.zip
$ vercel_blob --json put disk_dump.bin
{'url': 'https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/disk_dump-0eX9NYJnZjO31GsDiwDaQ6TR9QnWkH.bin', 'pathname': 'disk_dump.bin', 'contentType': 'text/plain', 'contentDisposition': 'attachment; filename="disk_dump.bin"'}
```
By default, Vercel's blob store will insert a randomly generated string to the name of your file. But you can turn off that feature:

```sh
$ vercel_blob put --no-suffix chart.png
------------------ -----------------------------------------------------------------
url https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/chart.png
pathname chart.png
contentType image/png
contentDisposition attachment; filename="chart.png"
------------------ -----------------------------------------------------------------
```

### List operation

The list method returns a list of blob objects in a Blob store. For example, let's upload a file to the bob store:
```sh
vercel_blob put profile.png
------------------ --------------------------------------------------------------------------------------------------
url https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/profile-OtpJ1AUIxChAA6UZejVXPA1pkuBw2D.png
pathname profile.png
contentType image/png
contentDisposition attachment; filename="profile.png"
------------------ --------------------------------------------------------------------------------------------------
```
Now you can see your file on your blob store:
```sh
vercel_blob list
Path name Size in bytes url
----------- --------------- -------------------------------------------------------------------
profile.png 11211 https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/profile-OtpJ1AUIxChAA6UZejVXPA1pkuBw2D.png
```
### Copy operation
```sh
vercel_storage blob copy <blob url> new/file/path/file.zip
$ vercel_blob copy https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/disk_dump-0eX9NYJnZjO31GsDiwDaQ6TR9QnWkH.bin file.zip
------------------ ----------------------------------------------------------------
url https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip
pathname file.zip
contentType application/octet-stream
contentDisposition attachment; filename="file.zip"
------------------ ----------------------------------------------------------------
```
You can also print the output information as a json:
```sh
vercel_storage --json copy <blob url> new/file/path/file.zip
$ vercel_blob --json copy https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/disk_dump-0eX9NYJnZjO31GsDiwDaQ6TR9QnWkH.bin file.zip
{'url': 'https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip', 'pathname': 'file.zip', 'contentType': 'application/octet-stream', 'contentDisposition': 'attachment; filename="file.zip"'}
```
### Delete operation
The delete operation always suceeds, regardless of whether the blob exists or not. It returns a null payload.
```sh
vercel_storage blob delete <blob url>
$ vercel_blob delete https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/disk_dump-mSjTcLOIg8hlGNiWpWMUcGqVll1uST.bin
```
### Head operation
The head operation returns a blob object's metadata.


```sh
$ vercel_blob head https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip
------------------ ----------------------------------------------------------------
url https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip
pathname file.zip
contentType application/octet-stream
contentDisposition attachment; filename="file.zip"
uploadedAt 2023-11-16T23:53:25.000Z
size 1998
cacheControl public, max-age=31536000, s-maxage=300
------------------ ----------------------------------------------------------------
```

As with the other commands, you can generate json output:

```sh
$ vercel_blob --json head https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip
{'url': 'https://c6zu0uktwgrh0d3g.public.blob.vercel-storage.com/file.zip', 'pathname': 'file.zip', 'contentType': 'application/octet-stream', 'contentDisposition': 'attachment; filename="file.zip"', 'uploadedAt': '2023-11-16T23:53:25.000Z', 'size': 1998, 'cacheControl': 'public, max-age=31536000, s-maxage=300'}
```


## Using vercel_storage in your python code

### List operation

Note: `vercel_storage` will look for the `BLOB_READ_WRITE_TOKEN` environment variable. If it is not available
it will raise an Exception.

If you have the token stored somewhere else, you can pass it directly to the put() function like this:


```python
resp = blob.list(options={'token': 'ABCD123foobar'})
```

### Put operation

```python
Expand All @@ -89,7 +179,7 @@ with open(my_file, 'rb') as fp:
)
```

`vercel_storage` will look for the `BLOB_READ_WRITE_TOKEN` environment variable. If it is not available
Note: `vercel_storage` will look for the `BLOB_READ_WRITE_TOKEN` environment variable. If it is not available
it will raise an Exception.

If you have the token stored somewhere else, you can pass it directly to the put() function like this:
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,26 @@ classifiers = [
]
dynamic = ["version", "description"]
requires-python = "~=3.9"

keywords = ["vercel", "api", "storage"]
dependencies = [
"requests >=2.6",
"click >= 8.1.7",
"tabulate >= 0.9.0"
]

[project.urls]
Source = "https://github.com/misaelnieto/vercel-storage"

[project.optional-dependencies]
test = [
"pytest",
"pytest-cov",
"requests-mock[fixture]",
"mypy"
]
packaging = [
"flit"
]

[project.scripts]
flit = "flit:main"
Expand Down
2 changes: 1 addition & 1 deletion vercel_storage/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pip install -e '.[test]'",
"postCreateCommand": "pip install -e '.[test]'"

// Configure tool-specific properties.
// "customizations": {},
Expand Down
80 changes: 50 additions & 30 deletions vercel_storage/blob.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from dataclasses import dataclass
from os import getenv
from typing import Any, Optional, Union
from mimetypes import guess_type
import urllib.parse

import requests
from tabulate import tabulate

from . import ConfigurationError, APIResponseError

Expand All @@ -14,29 +14,31 @@
API_VERSION = "4"
DEFAULT_CACHE_AGE = 365 * 24 * 60 * 60 # 1 Year
DEFAULT_ACCESS = "public"

DEFAULT_PAGE_SIZE = 100


def guess_mime_type(url):
return guess_type(url, strict=False)[0]


def get_token(options: dict):
if not options or not isinstance(options, dict):
_tkn = getenv(TOKEN_ENV, None)
else:
_tkn = options.get("token", None)
_tkn = options.get("token", getenv(TOKEN_ENV, None))
if not _tkn:
raise ConfigurationError("Vercel's BLOB_READ_WRITE_TOKEN is not set")
return _tkn


def dump_headers(options: dict, headers: dict):
if options.get("debug", False):
print(tabulate([(k, v) for k, v in headers.items()]))


def _coerce_bool(value):
return str(int(bool(value)))


def _handle_response(response: requests.Response):
if str(response.status_code) == '200':
if str(response.status_code) == "200":
return response.json()
raise APIResponseError(f"Oops, something went wrong: {response.json()}")

Expand All @@ -47,12 +49,15 @@ def put(pathname: str, body: bytes, options: Optional[dict] = None) -> dict:
"access": "public",
"authorization": f"Bearer {get_token(_opts)}",
"x-api-version": API_VERSION,
"x-content-type": _opts.get("contentType", "text/plain"),
"x-add-random-suffix": _coerce_bool(_opts.get("addRandomSuffix", True)),
"x-content-type": guess_mime_type(pathname),
"x-cache-control-max-age": _opts.get(
"cacheControlMaxAge", str(DEFAULT_CACHE_AGE)
),
}
if "no_suffix" in options:
headers["x-add-random-suffix"] = "false"

dump_headers(options, headers)
_resp = requests.put(f"{VERCEL_API_URL}/{pathname}", data=body, headers=headers)
return _handle_response(_resp)

Expand All @@ -72,7 +77,7 @@ def delete(
in Read-write token
Returns:
None: A delete action is always successful if the blob url exists.
None: A delete action is always successful if the blob url exists.
A delete action won't throw if the blob url doesn't exists.
"""
_opts = dict(options) if options else dict()
Expand All @@ -81,6 +86,7 @@ def delete(
"x-api-version": API_VERSION,
"content-type": "application/json",
}
dump_headers(options, headers)
_resp = requests.post(
f"{VERCEL_API_URL}/delete",
json={
Expand All @@ -104,18 +110,34 @@ def list(options: Optional[dict] = None) -> Any:
use when making requests. It defaults to the BLOB_READ_WRITE_TOKEN
environment variable when deployed on Vercel as explained
in Read-write token
limit
prefix
cursor
mode
limit (Not required): A number specifying the maximum number of
blob objects to return. It defaults to 1000
prefix (Not required): A string used to filter for blob objects
contained in a specific folder assuming that the folder name was
used in the pathname when the blob object was uploaded
cursor (Not required): A string obtained from a previous response for pagination
of retults
mode (Not required): A string specifying the response format. Can
either be "expanded" (default) or "folded". In folded mode
all blobs that are located inside a folder will be folded into
a single folder string entry
Returns:
???
Json response
"""
_opts = dict(options) if options else dict()
headers = {
"authorization": f"Bearer {get_token(_opts)}",
"limit": _opts.get("limit", str(DEFAULT_PAGE_SIZE)),
}
if "prefix" in _opts:
headers["prefix"] = _opts["prefix"]
if "cursor" in _opts:
headers["cursor"] = _opts["cursor"]
if "mode" in _opts:
headers["mode"] = _opts["mode"]

dump_headers(options, headers)
_resp = requests.get(
f"{VERCEL_API_URL}",
headers=headers,
Expand Down Expand Up @@ -143,24 +165,21 @@ def head(url: str, options: Optional[dict] = None) -> dict:
"authorization": f"Bearer {get_token(_opts)}",
"x-api-version": API_VERSION,
}
_resp = requests.get(
f"{VERCEL_API_URL}",
headers=headers,
params={'url': url}
)
dump_headers(options, headers)
_resp = requests.get(f"{VERCEL_API_URL}", headers=headers, params={"url": url})
return _handle_response(_resp)


def copy(from_url: str, to_pathname: str, options: Optional[dict] = None) -> dict:
"""
Copies an existing blob object to a new path inside the blob store.
The contentType and cacheControlMaxAge will not be copied from the source
blob. If the values should be carried over to the copy, they need to be
The contentType and cacheControlMaxAge will not be copied from the source
blob. If the values should be carried over to the copy, they need to be
defined again in the options object.
Contrary to put(), addRandomSuffix is false by default. This means no
automatic random id suffix is added to your blob url, unless you pass
Contrary to put(), addRandomSuffix is false by default. This means no
automatic random id suffix is added to your blob url, unless you pass
addRandomSuffix: True. This also means copy() overwrites files per default,
if the operation targets a pathname that already exists.
Expand All @@ -173,12 +192,12 @@ def copy(from_url: str, to_pathname: str, options: Optional[dict] = None) -> dic
use when making requests. It defaults to the BLOB_READ_WRITE_TOKEN
environment variable when deployed on Vercel as explained
in Read-write token
contentType (Not required): A string indicating the media type.
contentType (Not required): A string indicating the media type.
By default, it's extracted from the to_pathname's extension.
addRandomSuffix (Not required): A boolean specifying whether to add
a random suffix to the pathname. It defaults to False.
cacheControlMaxAge (Not required): A number in seconds to configure
the edge and browser cache. Defaults to one year. See Vercel's
the edge and browser cache. Defaults to one year. See Vercel's
caching documentation for more details.
"""
_opts = dict(options) if options else dict()
Expand All @@ -192,8 +211,9 @@ def copy(from_url: str, to_pathname: str, options: Optional[dict] = None) -> dic
"cacheControlMaxAge", str(DEFAULT_CACHE_AGE)
),
}
dump_headers(options, headers)
_to = urllib.parse.quote(to_pathname)
resp = requests.put(f"{VERCEL_API_URL}/{_to}", headers=headers, params={
'fromUrl': from_url
})
return _handle_response(resp)
resp = requests.put(
f"{VERCEL_API_URL}/{_to}", headers=headers, params={"fromUrl": from_url}
)
return _handle_response(resp)
Loading

0 comments on commit 71ce949

Please sign in to comment.