sobe 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sobe might be problematic. Click here for more details.
- sobe/__init__.py +0 -0
- sobe/main.py +153 -0
- sobe-0.1.dist-info/METADATA +93 -0
- sobe-0.1.dist-info/RECORD +6 -0
- sobe-0.1.dist-info/WHEEL +4 -0
- sobe-0.1.dist-info/entry_points.txt +3 -0
sobe/__init__.py
ADDED
|
File without changes
|
sobe/main.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import functools
|
|
4
|
+
import json
|
|
5
|
+
import mimetypes
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import tomllib
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import boto3
|
|
13
|
+
import botocore.exceptions
|
|
14
|
+
import platformdirs
|
|
15
|
+
import urllib3.exceptions
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config():
|
|
19
|
+
path = platformdirs.PlatformDirs("sobe", "balbuena.ca").user_config_path / "config.toml"
|
|
20
|
+
if path.exists():
|
|
21
|
+
with path.open("rb") as f:
|
|
22
|
+
payload = tomllib.load(f)
|
|
23
|
+
if payload["bucket"] != "example-bucket":
|
|
24
|
+
return payload
|
|
25
|
+
|
|
26
|
+
defaults = """
|
|
27
|
+
# sobe configuration
|
|
28
|
+
bucket = "example-bucket"
|
|
29
|
+
url = "https://example.com/"
|
|
30
|
+
cloudfront = "E1111111111111"
|
|
31
|
+
|
|
32
|
+
[aws_session]
|
|
33
|
+
# If you already have AWS CLI set up, don't fill keys here.
|
|
34
|
+
# region_name = "..."
|
|
35
|
+
# profile_name = "..."
|
|
36
|
+
# aws_access_key_id = "..."
|
|
37
|
+
# aws_secret_access_key = "..."
|
|
38
|
+
|
|
39
|
+
[aws_client]
|
|
40
|
+
verify = true
|
|
41
|
+
"""
|
|
42
|
+
defaults = "\n".join(line.strip() for line in defaults.lstrip().splitlines())
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
path.write_text(defaults)
|
|
45
|
+
print("Created config file at the path below. You must edit it before use.")
|
|
46
|
+
print(path)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CONFIG = load_config()
|
|
51
|
+
write = functools.partial(print, flush=True, end="")
|
|
52
|
+
print = functools.partial(print, flush=True) # type: ignore
|
|
53
|
+
warnings.filterwarnings("ignore", category=urllib3.exceptions.InsecureRequestWarning)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
args = parse_args()
|
|
58
|
+
session = boto3.Session(**CONFIG["aws_session"])
|
|
59
|
+
bucket = session.resource("s3", **CONFIG["aws_client"]).Bucket(CONFIG["bucket"])
|
|
60
|
+
for path, key in zip(args.paths, args.keys):
|
|
61
|
+
if args.delete:
|
|
62
|
+
delete(bucket, key)
|
|
63
|
+
else:
|
|
64
|
+
upload(bucket, path, key)
|
|
65
|
+
if args.invalidate:
|
|
66
|
+
invalidate(session)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def upload(bucket, path: pathlib.Path, remote_path: str) -> None:
|
|
70
|
+
write(f"{CONFIG['url']}{remote_path} ...")
|
|
71
|
+
type_guess, _ = mimetypes.guess_type(path)
|
|
72
|
+
extra_args = {"ContentType": type_guess or "application/octet-stream"}
|
|
73
|
+
bucket.upload_file(str(path), remote_path, ExtraArgs=extra_args)
|
|
74
|
+
print("ok.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def delete(bucket, remote_path: str) -> None:
|
|
78
|
+
write(f"{CONFIG['url']}{remote_path} ...")
|
|
79
|
+
obj = bucket.Object(remote_path)
|
|
80
|
+
try:
|
|
81
|
+
obj.load()
|
|
82
|
+
obj.delete()
|
|
83
|
+
print("deleted.")
|
|
84
|
+
except botocore.exceptions.ClientError as e:
|
|
85
|
+
if e.response["Error"]["Code"] != "404":
|
|
86
|
+
raise
|
|
87
|
+
print("didn't exist.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def invalidate(session: boto3.Session) -> None:
|
|
91
|
+
write("Clearing cache ...")
|
|
92
|
+
ref = datetime.datetime.now().astimezone().isoformat()
|
|
93
|
+
cloudfront = session.client("cloudfront", **CONFIG["aws_client"])
|
|
94
|
+
batch = {"Paths": {"Quantity": 1, "Items": ["/*"]}, "CallerReference": ref}
|
|
95
|
+
invalidation = cloudfront.create_invalidation(DistributionId=CONFIG["cloudfront"], InvalidationBatch=batch)
|
|
96
|
+
write("ok.")
|
|
97
|
+
invalidation_id = invalidation["Invalidation"]["Id"]
|
|
98
|
+
status = ""
|
|
99
|
+
while status != "Completed":
|
|
100
|
+
time.sleep(3)
|
|
101
|
+
write(".")
|
|
102
|
+
response = cloudfront.get_invalidation(DistributionId=CONFIG["cloudfront"], Id=invalidation_id)
|
|
103
|
+
status = response["Invalidation"]["Status"]
|
|
104
|
+
print("complete.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_args() -> argparse.Namespace:
|
|
108
|
+
parser = argparse.ArgumentParser(description="Upload files to your AWS drop box.")
|
|
109
|
+
parser.add_argument("-y", "--year", type=int, default=datetime.date.today().year, help="change year directory")
|
|
110
|
+
parser.add_argument("-i", "--invalidate", action="store_true", help="invalidate CloudFront cache")
|
|
111
|
+
parser.add_argument("-d", "--delete", action="store_true", help="delete instead of upload")
|
|
112
|
+
parser.add_argument("--policy", action="store_true", help="display IAM policy requirements and exit")
|
|
113
|
+
parser.add_argument("files", nargs="*", help="Source files.")
|
|
114
|
+
args = parser.parse_args()
|
|
115
|
+
|
|
116
|
+
if args.policy:
|
|
117
|
+
dump_policy()
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
if not args.files and not args.invalidate:
|
|
121
|
+
parser.print_help()
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
args.paths = [pathlib.Path(p) for p in args.files]
|
|
125
|
+
args.keys = [f"{args.year}/{p.name}" for p in args.paths]
|
|
126
|
+
if not args.delete:
|
|
127
|
+
missing = [p for p in args.paths if not p.exists()]
|
|
128
|
+
if missing:
|
|
129
|
+
print("The following files do not exist:")
|
|
130
|
+
for p in missing:
|
|
131
|
+
print(f" {p}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
return args
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def dump_policy() -> None:
|
|
138
|
+
session = boto3.Session(**CONFIG["aws_session"])
|
|
139
|
+
sts = session.client("sts", **CONFIG["aws_client"])
|
|
140
|
+
caller = sts.get_caller_identity()["Arn"]
|
|
141
|
+
account_id = caller.split(":")[4]
|
|
142
|
+
actions = """
|
|
143
|
+
s3:PutObject s3:GetObject s3:ListBucket s3:DeleteObject
|
|
144
|
+
cloudfront:CreateInvalidation cloudfront:GetInvalidation
|
|
145
|
+
""".split()
|
|
146
|
+
resources = [
|
|
147
|
+
f"arn:aws:s3:::{CONFIG['bucket']}",
|
|
148
|
+
f"arn:aws:s3:::{CONFIG['bucket']}/*",
|
|
149
|
+
f"arn:aws:cloudfront::{account_id}:distribution/{CONFIG['cloudfront']}",
|
|
150
|
+
]
|
|
151
|
+
statement = {"Effect": "Allow", "Action": actions, "Resource": resources}
|
|
152
|
+
policy = {"Version": "2012-10-17", "Statement": [statement]}
|
|
153
|
+
print(json.dumps(policy, indent=2))
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sobe
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: AWS-based drop box uploader
|
|
5
|
+
Requires-Dist: boto3>=1.40.49
|
|
6
|
+
Requires-Dist: platformdirs>=4.5.0
|
|
7
|
+
Requires-Python: ==3.13.*
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# sobe
|
|
11
|
+
|
|
12
|
+
A simple command-line tool for uploading files to an AWS S3 bucket that is publicly available through a CloudFront distribution. This is the traditional "drop box" use case that existed long before the advent of modern file sharing services.
|
|
13
|
+
|
|
14
|
+
It will upload any files you give it to your bucket, in a current year subdirectory, because that's the only easy way to organize chaos.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Use [uv](https://docs.astral.sh/uv/) to manage it.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv tool install https://github.com/Liz4v/sobe.git
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
On first run, `sobe` will create its config file as appropriate to the platform. You'll need to edit this file with your AWS bucket and CloudFront details:
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
# sobe configuration
|
|
30
|
+
bucket = "your-bucket-name"
|
|
31
|
+
url = "https://your-public-url/"
|
|
32
|
+
cloudfront = "your-cloudfront-distribution-id"
|
|
33
|
+
|
|
34
|
+
[aws_session]
|
|
35
|
+
# If you already have AWS CLI set up, don't fill keys here.
|
|
36
|
+
# region_name = "..."
|
|
37
|
+
# profile_name = "..."
|
|
38
|
+
# aws_access_key_id = "..."
|
|
39
|
+
# aws_secret_access_key = "..."
|
|
40
|
+
|
|
41
|
+
[aws_client]
|
|
42
|
+
verify = true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
sobe [options] files...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
|
|
53
|
+
- `-y`, `--year`: Change the target year directory (default: current year)
|
|
54
|
+
- `-i`, `--invalidate`: Invalidate CloudFront cache after upload
|
|
55
|
+
- `-d`, `--delete`: Delete files instead of uploading
|
|
56
|
+
- `-p`, `--policy`: Display required AWS IAM policy and exit
|
|
57
|
+
|
|
58
|
+
### Examples
|
|
59
|
+
|
|
60
|
+
Upload files to current year directory:
|
|
61
|
+
```bash
|
|
62
|
+
sobe file1.jpg file2.pdf
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Upload files to a specific year:
|
|
66
|
+
```bash
|
|
67
|
+
sobe -y 2024 file1.jpg file2.pdf
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Upload and invalidate CloudFront cache:
|
|
71
|
+
```bash
|
|
72
|
+
sobe -i file1.jpg
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Delete files:
|
|
76
|
+
```bash
|
|
77
|
+
sobe -d file1.jpg
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Get required AWS IAM policy:
|
|
81
|
+
```bash
|
|
82
|
+
sobe --policy
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## AWS Permissions
|
|
86
|
+
|
|
87
|
+
Use `sobe --policy` to generate the exact IAM policy required for your configuration. The tool needs permissions for:
|
|
88
|
+
- S3: PutObject, GetObject, ListBucket, DeleteObject
|
|
89
|
+
- CloudFront: CreateInvalidation, GetInvalidation
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sobe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sobe/main.py,sha256=3x3wsfo4l_rh7Nr-GEsL3jiqU8jTrYINkojzvKTB1eI,5232
|
|
3
|
+
sobe-0.1.dist-info/WHEEL,sha256=ZbtZh9LqsQoZs-WmwRO6z-tavdkb5LzNxvrOv2F_OXE,78
|
|
4
|
+
sobe-0.1.dist-info/entry_points.txt,sha256=a_cKExqUEzJ-t2MRWbxAHc8OavQIaL8a7JQ3obR2b-c,41
|
|
5
|
+
sobe-0.1.dist-info/METADATA,sha256=pd-fXFiH_aB6x8dr-tav4oUE4vtgE4FSlQ-_1FJ0ZSw,2219
|
|
6
|
+
sobe-0.1.dist-info/RECORD,,
|
sobe-0.1.dist-info/WHEEL
ADDED