Skip to content

Commit 0cdd5e4

Browse files
committed
Add fiocli <- command line uploader/downloader
1 parent 5b628a9 commit 0cdd5e4

File tree

10 files changed

+259
-56
lines changed

10 files changed

+259
-56
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ $ git clone https://github.com/frameio/python-frameio-client
2424
$ pip install .
2525
```
2626

27+
### Developing
28+
Install the package into your development environment and link to it by running the following:
29+
30+
```sh
31+
pipenv install -e . -pre
32+
```
33+
2734
## Documentation
2835

2936
[Frame.io API Documentation](https://developer.frame.io/docs)
@@ -34,6 +41,27 @@ _Note: A valid token is required to make requests to Frame.io. Go to our [Develo
3441

3542
In addition to the snippets below, examples are included in [/examples](/examples).
3643

44+
### Use CLI
45+
When you install this package, a cli tool called `fioctl` will also be installed to your environment.
46+
47+
**To upload a file or folder**
48+
```sh
49+
fioctl \
50+
--token fio-u-YOUR_TOKEN_HERE \
51+
--destination "YOUR TARGET FRAME.IO PROJECT OR FOLDER" \
52+
--target "YOUR LOCAL SYSTEM DIRECTORY" \
53+
--threads 8
54+
```
55+
56+
**To download a file, project, or folder**
57+
```sh
58+
fioctl \
59+
--token fio-u-YOUR_TOKEN_HERE \
60+
--destination "YOUR LOCAL SYSTEM DIRECTORY" \
61+
--target "YOUR TARGET FRAME.IO PROJECT OR FOLDER" \
62+
--threads 2
63+
```
64+
3765
### Get User Info
3866

3967
Get basic info on the authenticated user.

examples/recursive_upload.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
import time
3+
import mimetypes
4+
import concurrent.futures
5+
import threading
6+
from frameioclient import FrameioClient
7+
from pprint import pprint
8+
9+
global file_num
10+
file_num = 0
11+
12+
global file_count
13+
file_count = 0
14+
15+
def create_n_upload(task):
16+
client=task[0]
17+
file_p=task[1]
18+
parent_asset_id=task[2]
19+
abs_path = os.path.abspath(file_p)
20+
file_s = os.path.getsize(file_p)
21+
file_n = os.path.split(file_p)[1]
22+
file_mime = mimetypes.guess_type(abs_path)[0]
23+
24+
asset = client.create_asset(
25+
parent_asset_id=parent_asset_id,
26+
name=file_n,
27+
type="file",
28+
filetype=file_mime,
29+
filesize=file_s
30+
)
31+
32+
with open(abs_path, "rb") as ul_file:
33+
asset_info = client.upload(asset, ul_file)
34+
35+
return asset_info
36+
37+
38+
def create_folder(folder_n, parent_asset_id):
39+
asset = client.create_asset(
40+
parent_asset_id=parent_asset_id,
41+
name=folder_n,
42+
type="folder",
43+
)
44+
45+
return asset['id']
46+
47+
48+
def file_counter(root_folder):
49+
matches = []
50+
for root, dirnames, filenames in os.walk(root_folder):
51+
for filename in filenames:
52+
matches.append(os.path.join(filename))
53+
54+
return matches
55+
56+
57+
def recursive_upload(client, folder, parent_asset_id):
58+
# Seperate files and folders:
59+
file_list = list()
60+
folder_list = list()
61+
62+
for item in os.listdir(folder):
63+
if item == ".DS_Store": # Ignore .DS_Store files on Mac
64+
continue
65+
66+
complete_item_path = os.path.join(folder, item)
67+
68+
if os.path.isfile(complete_item_path):
69+
file_list.append(item)
70+
else:
71+
folder_list.append(item)
72+
73+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
74+
for file_p in file_list:
75+
global file_num
76+
file_num += 1
77+
print(f"Starting {file_num}/{file_count}")
78+
complete_dir_obj = os.path.join(folder, file_p)
79+
task = (client, complete_dir_obj, parent_asset_id)
80+
executor.submit(create_n_upload, task)
81+
82+
for folder_i in folder_list:
83+
new_folder = os.path.join(folder, folder_i)
84+
new_parent_asset_id = create_folder(folder_i, parent_asset_id)
85+
recursive_upload(client, new_folder, new_parent_asset_id)
86+
87+
88+
if __name__ == "__main__":
89+
root_folder = "./test_structure"
90+
parent_asset_id = "PARENT_ASSET_ID"
91+
client = FrameioClient(os.getenv("FRAME_IO_TOKEN"))
92+
93+
file_count = len(file_counter(root_folder))
94+
recursive_upload(client, root_folder, parent_asset_id)

examples/upload_asset.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import os
12
from frameioclient import FrameioClient
23

34
def main():
4-
client = FrameioClient("TOKEN")
5-
file_path = './file_to_upload.mov'
6-
parent_asset_id = ''
5+
client = FrameioClient(os.getenv('FRAMEIO_TOKEN'))
6+
file_path = '/Users/jeff/downloads/📼 Video/🎨 Graded Dailies/A001C034_210518_ROGH.mov'
7+
parent_asset_id = 'b91433a2-8c9d-4da4-a9dd-80649cb91adf'
78

8-
asset = client.assets.upload(parent_asset_id,file_path)
9+
# asset = client.assets.upload(parent_asset_id,file_path)
10+
client.assets.upload(parent_asset_id, file_path)
911

1012
if __name__ == "__main__":
1113
main()

frameioclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .lib import *
2-
from .services import *
2+
from .service import *
33
from .client import FrameioClient

frameioclient/client.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
)
88

99
class FrameioClient(APIClient, object):
10-
def __init__(self, token, host='https://api.frame.io'):
11-
super().__init__(token, host)
10+
def __init__(self, token, host='https://api.frame.io', threads=5, progress=False):
11+
super().__init__(token, host, threads, progress)
1212

1313
@property
1414
def me(self):
@@ -32,40 +32,40 @@ def _download(self):
3232

3333
@property
3434
def users(self):
35-
from .services import User
35+
from .service import User
3636
return User(self)
3737

3838
@property
3939
def assets(self):
40-
from .services import Asset
40+
from .service import Asset
4141
return Asset(self)
4242

4343
@property
4444
def comments(self):
45-
from .services import Comment
45+
from .service import Comment
4646
return Comment(self)
4747

4848
@property
4949
def logs(self):
50-
from .services import AuditLogs
50+
from .service import AuditLogs
5151
return AuditLogs(self)
5252

5353
@property
5454
def review_links(self):
55-
from .services import ReviewLink
55+
from .service import ReviewLink
5656
return ReviewLink(self)
5757

5858
@property
5959
def presentation_links(self):
60-
from .services import PresentationLink
60+
from .service import PresentationLink
6161
return PresentationLink(self)
6262

6363
@property
6464
def projects(self):
65-
from .services import Project
65+
from .service import Project
6666
return Project(self)
6767

6868
@property
6969
def teams(self):
70-
from .services import Team
70+
from .service import Team
7171
return Team(self)

frameioclient/fioctl.py renamed to frameioclient/fiocli.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,45 @@
66

77

88
def main():
9-
parser=argparse.ArgumentParser(prog='fioctl', description='Frame.io Python SDK CLI')
9+
parser=argparse.ArgumentParser(prog='fiocli', description='Frame.io Python SDK CLI')
1010

1111
## Define args
1212
parser.add_argument('--token', action='store', metavar='token', type=str, nargs='+', help='Developer Token')
13-
parser.add_argument('--op', action='store', metavar='op', type=str, nargs='+', help='Operation: upload, download')
13+
# parser.add_argument('--op', action='store', metavar='op', type=str, nargs='+', help='Operation: upload, download')
1414
parser.add_argument('--target', action='store', metavar='target', type=str, nargs='+', help='Target: remote project or folder, or alternatively a local file/folder')
1515
parser.add_argument('--destination', action='store', metavar='destination', type=str, nargs='+', help='Destination: remote project or folder, or alternatively a local file/folder')
16+
parser.add_argument('--threads', action='store', metavar='threads', type=int, nargs='+', help='Number of threads to use')
1617

1718
## Parse args
1819
args = parser.parse_args()
1920

20-
# ## Handle args
21+
if args.threads:
22+
threads = args.threads[0]
23+
else:
24+
threads = 5
25+
26+
## Handle args
2127
if args.token:
2228
client = None
29+
# print(args.token)
2330
try:
24-
client = FrameioClient("fio-u-dVwbtNcxXCxySPRIRadPflefXpV-NgVD4vqR3EGIWg20xIkGd_6rQ7pDNFA5YOCy")
31+
client = FrameioClient(args.token[0], progress=True, threads=threads)
2532
except Exception as e:
2633
print("Failed")
2734
sys.exit(1)
2835

29-
# if args.op == 'upload':
36+
# If args.op == 'upload':
3037
if args.target:
3138
if args.destination:
3239
# Check to see if this is a local target and thus a download
3340
if os.path.isdir(args.destination[0]):
34-
return client.assets.download(args.target[0], args.destination[0])
41+
asset = client.assets.get(args.target[0])
42+
return client.assets.download(asset, args.destination[0], progress=True, multi_part=True, concurrency=threads)
3543
else: # This is an upload
36-
return client.assets.upload(args.destination[0], args.target[0])
44+
if os.path.isdir(args.target[0]):
45+
return client.assets.upload_folder(args.target[0], args.destination[0])
46+
else:
47+
return client.assets.upload(args.destination[0], args.target[0])
3748
else:
3849
print("No destination supplied")
3950
else:

frameioclient/lib/transport.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ def _get_session(self):
4444

4545

4646
class APIClient(HTTPClient, object):
47-
def __init__(self, token, host):
47+
def __init__(self, token, host, threads, progress):
4848
super().__init__()
4949
self.host = host
5050
self.token = token
51+
self.threads = threads
52+
self.progress = progress
5153
self._initialize_thread()
5254
self.session = self._get_session()
5355
self.auth_header = {
@@ -84,8 +86,8 @@ def _api_call(self, method, endpoint, payload={}, limit=None):
8486

8587
return r.json()
8688

87-
if r.status_code == 422 and "presentation" in endpoint:
88-
raise PresentationException
89+
# if r.status_code == 422 and "presentation" in endpoint:
90+
# raise PresentationException
8991

9092
return r.raise_for_status()
9193

frameioclient/lib/upload.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
import threading
55
import concurrent.futures
66

7+
from .utils import Utils
8+
79
thread_local = threading.local()
810

911
class FrameioUploader(object):
10-
def __init__(self, asset, file):
12+
def __init__(self, asset=None, file=None):
1113
self.asset = asset
1214
self.file = file
1315
self.chunk_size = None
16+
self.file_count = 0
17+
self.file_num = 0
1418

1519
def _calculate_chunks(self, total_size, chunk_count):
1620
self.chunk_size = int(math.ceil(total_size / chunk_count))
@@ -76,3 +80,49 @@ def upload(self):
7680

7781
task = (url, chunk_offset, i)
7882
executor.submit(self._upload_chunk, task)
83+
84+
def file_counter(self, folder):
85+
matches = []
86+
for root, dirnames, filenames in os.walk(folder):
87+
for filename in filenames:
88+
matches.append(os.path.join(filename))
89+
90+
self.file_count = len(matches)
91+
92+
return matches
93+
94+
def recursive_upload(self, client, folder, parent_asset_id):
95+
# Seperate files and folders:
96+
file_list = list()
97+
folder_list = list()
98+
99+
if self.file_count == 0:
100+
self.file_counter(folder)
101+
102+
for item in os.listdir(folder):
103+
if item == ".DS_Store": # Ignore .DS_Store files on Mac
104+
continue
105+
106+
complete_item_path = os.path.join(folder, item)
107+
108+
if os.path.isfile(complete_item_path):
109+
file_list.append(item)
110+
else:
111+
folder_list.append(item)
112+
113+
for file_p in file_list:
114+
self.file_num += 1
115+
116+
complete_dir_obj = os.path.join(folder, file_p)
117+
print(f"Starting {self.file_num:02d}/{self.file_count}, Size: {Utils.format_bytes(os.path.getsize(complete_dir_obj), type='size')}, Name: {file_p}")
118+
client.assets.upload(parent_asset_id, complete_dir_obj)
119+
120+
for folder_name in folder_list:
121+
new_folder = os.path.join(folder, folder_name)
122+
new_parent_asset_id = client.assets.create(
123+
parent_asset_id=parent_asset_id,
124+
name=folder_name,
125+
type="folder"
126+
)['id']
127+
128+
self.recursive_upload(client, new_folder, new_parent_asset_id)

0 commit comments

Comments
 (0)