Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

retrieve complete album list in _fetch_folders #410

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ad14bab
retrieve complete album list in _fetch_folders
gordonaspin Nov 15, 2022
8de424f
implement item_type
gordonaspin Nov 21, 2022
a265d94
add .python-version
gordonaspin Nov 22, 2022
e0467d7
resolved relative path
gordonaspin Nov 28, 2022
fbd8293
added live movie support
gordonaspin Nov 30, 2022
a9c2ab2
re-implement exception handler
gordonaspin Dec 5, 2022
2a64349
logging
gordonaspin Dec 6, 2022
6652d30
added scripts/build
gordonaspin Dec 6, 2022
6177cd7
updated setup.py
gordonaspin Dec 6, 2022
fff67dd
fix 2sa url from 2sv
gordonaspin Dec 6, 2022
e26ac80
resolve 2sa occasional 404
gordonaspin Dec 6, 2022
fc59736
revert logger/LOGGER and fix 2sv/2sa typo
gordonaspin Dec 9, 2022
d25545b
added --log-level command line option
gordonaspin Dec 9, 2022
7726ac1
remove redundant looger statement
gordonaspin Dec 13, 2022
a577631
lower case file extensions
gordonaspin Jan 3, 2023
cf53acf
fixed item_type choice in .versions
gordonaspin Jan 3, 2023
16f9be7
exception handling
gordonaspin Jan 5, 2023
84477e7
password length in filter
gordonaspin Jan 6, 2023
48b21fe
removed verbose logging of cookies/session
gordonaspin Jan 22, 2023
6be05c8
pass client_id to drive service
gordonaspin Feb 21, 2023
f62d25e
changed delete() to actually deletes the node
gordonaspin Mar 15, 2023
91f7643
Update drive.py
gordonaspin Mar 15, 2023
90f2817
suppress parse warning on status 204
gordonaspin Apr 3, 2023
a580362
added item_extention types
gordonaspin Sep 13, 2023
8e749fe
python3.12
gordonaspin Jan 8, 2024
786930c
clean up in remove()
gordonaspin Aug 1, 2024
9630640
update
gordonaspin Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*.egg
*.egg-info
dist
build
build/*
eggs
.eggs
parts
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.1
5 changes: 5 additions & 0 deletions .venv/pyvenv.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
home = /home/gordon/.pyenv/versions/3.12.1/bin
include-system-site-packages = false
version = 3.12.1
executable = /home/gordon/.pyenv/versions/3.12.1/bin/python3.12
command = /home/gordon/Documents/Development/ml/.venv/bin/python -m venv /home/gordon/Documents/Development/pyicloud/.venv
16 changes: 16 additions & 0 deletions pyicloud/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "command line",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
21 changes: 15 additions & 6 deletions pyicloud/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self, password):
def filter(self, record):
message = record.getMessage()
if self.name in message:
record.msg = message.replace(self.name, "*" * 8)
record.msg = message.replace(self.name, "*" * len(self.name))
record.args = []

return True
Expand Down Expand Up @@ -90,11 +90,11 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
# Save session_data to file
with open(self.service.session_path, "w", encoding="utf-8") as outfile:
json.dump(self.service.session_data, outfile)
LOGGER.debug("Saved session data to file")
#LOGGER.debug("Saved session data to file")

# Save cookies to file
self.cookies.save(ignore_discard=True, ignore_expires=True)
LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path)
#LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path)

if not response.ok and (
content_type not in json_mimetypes
Expand Down Expand Up @@ -138,7 +138,10 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
try:
data = response.json()
except: # pylint: disable=bare-except
request_logger.warning("Failed to parse response with JSON mimetype")
if response.status_code == 204:
pass
else:
request_logger.warning(f"Failed to parse response {response} with JSON mimetype")
return response

request_logger.debug(data)
Expand Down Expand Up @@ -268,6 +271,14 @@ def __init__(
# successful authentication.
LOGGER.warning("Failed to read cookiejar %s", cookiejar_path)

self.params = {
# 'clientBuildNumber': '17DHotfix5',
# 'clientMasteringNumber': '17DHotfix5',
# 'ckjsBuildVersion': '17DProjectDev77',
# 'ckjsVersion': '2.0.5',
'clientId': self.client_id,
}

self.authenticate()

self._drive = None
Expand All @@ -282,7 +293,6 @@ def authenticate(self, force_refresh=False, service=None):

login_successful = False
if self.session_data.get("session_token") and not force_refresh:
LOGGER.debug("Checking session token validity")
try:
self.data = self._validate_token()
login_successful = True
Expand Down Expand Up @@ -376,7 +386,6 @@ def _authenticate_with_credentials_service(self, service):

def _validate_token(self):
"""Checks if the current access token is still valid."""
LOGGER.debug("Checking session token validity")
try:
req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null")
LOGGER.debug("Session token is still valid")
Expand Down
29 changes: 28 additions & 1 deletion pyicloud/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import argparse
import pickle
import sys
import logging

from click import confirm

from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from . import utils
import pyicloud.utils as utils

DEVICE_ERROR = "Please use the --device switch to indicate which device to use."

logger = logging.getLogger("pyicloud")


def create_pickled_data(idevice, filename):
"""
Expand Down Expand Up @@ -163,11 +166,35 @@ def main(args=None):
default="",
help="Save device data to a file in the current directory.",
)
parser.add_argument(
"--log-level",
action="store",
dest="loglevel",
default="none",
help="set loglevel debug/info/error",
)

command_line = parser.parse_args(args)

username = command_line.username
password = command_line.password
loglevel = command_line.loglevel

if not loglevel == "none":
formatter = logging.Formatter(fmt="%(asctime)s %(levelname)-8s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
stdout_handler = logging.StreamHandler(stream=sys.stdout)
stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)

if loglevel == "info":
logger.setLevel(logging.INFO)
logger.info("log-level INFO")
elif loglevel == "debug":
logger.setLevel(logging.DEBUG)
logger.info("log-level DEBUG")
elif loglevel == "error":
logger.setLevel(logging.ERROR)
logger.info("log-level ERROR")

if username and command_line.delete_from_keyring:
utils.delete_password_in_keyring(username)
Expand Down
58 changes: 46 additions & 12 deletions pyicloud/services/drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def get_app_data(self):
self._raise_if_error(request)
return request.json()["items"]

def _get_upload_contentws_url(self, file_object):
def _get_upload_contentws_url(self, file_object, **kwargs):
"""Get the contentWS endpoint URL to add a new file."""
content_type = mimetypes.guess_type(file_object.name)[0]
if content_type is None:
Expand Down Expand Up @@ -108,7 +108,7 @@ def _get_upload_contentws_url(self, file_object):
self._raise_if_error(request)
return (request.json()[0]["document_id"], request.json()[0]["url"])

def _update_contentws(self, folder_id, sf_info, document_id, file_object):
def _update_contentws(self, folder_id, sf_info, document_id, file_object, **kwargs):
data = {
"data": {
"signature": sf_info["fileChecksum"],
Expand All @@ -129,8 +129,8 @@ def _update_contentws(self, folder_id, sf_info, document_id, file_object):
"is_executable": False,
"is_hidden": False,
},
"mtime": int(time.time() * 1000),
"btime": int(time.time() * 1000),
"mtime": int(kwargs.get("mtime", time.time()) * 1000),
"btime": int(kwargs.get("ctime", time.time()) * 1000),
}

# Add the receipt if we have one. Will be absent for 0-sized files
Expand All @@ -146,14 +146,14 @@ def _update_contentws(self, folder_id, sf_info, document_id, file_object):
self._raise_if_error(request)
return request.json()

def send_file(self, folder_id, file_object):
def send_file(self, folder_id, file_object, **kwargs):
"""Send new file to iCloud Drive."""
document_id, content_url = self._get_upload_contentws_url(file_object)
document_id, content_url = self._get_upload_contentws_url(file_object, **kwargs)

request = self.session.post(content_url, files={file_object.name: file_object})
self._raise_if_error(request)
content_response = request.json()["singleFile"]
self._update_contentws(folder_id, content_response, document_id, file_object)
self._update_contentws(folder_id, content_response, document_id, file_object, **kwargs)

def create_folders(self, parent, name):
"""Creates a new iCloud Drive folder"""
Expand Down Expand Up @@ -216,6 +216,26 @@ def move_items_to_trash(self, node_id, etag):
self._raise_if_error(request)
return request.json()

def delete_items(self, node_id, etag):
"""Deletes an iCloud Drive node"""
request = self.session.post(
self._service_root + "/deleteItems",
params=self.params,
data=json.dumps(
{
"items": [
{
"drivewsid": node_id,
"etag": etag,
"clientId": self.params["clientId"],
}
],
}
),
)
self._raise_if_error(request)
return request.json()

@property
def root(self):
"""Returns the root node."""
Expand Down Expand Up @@ -259,10 +279,10 @@ def type(self):
node_type = self.data.get("type")
return node_type and node_type.lower()

def get_children(self):
def get_children(self, force=False):
"""Gets the node children."""
if not self._children:
if "items" not in self.data:
if not self._children or force:
if "items" not in self.data or force:
self.data.update(self.connection.get_node_data(self.data["docwsid"]))
if "items" not in self.data:
raise KeyError("No items in folder, status: %s" % self.data["status"])
Expand All @@ -272,6 +292,14 @@ def get_children(self):
]
return self._children

def remove(self, child):
for item_data in self.data['items']:
if item_data['docwsid'] == child.data['docwsid']:
self.data['items'].remove(item_data)
break
self._children.remove(child)
return

@property
def size(self):
"""Gets the node size."""
Expand Down Expand Up @@ -324,9 +352,15 @@ def rename(self, name):
self.data["drivewsid"], self.data["etag"], name
)

def move_to_trash(self):
"""Move an iCloud Drive item to the trash bin (Recently Deleted)."""
return self.connection.move_items_to_trash(
self.data["drivewsid"], self.data["etag"]
)

def delete(self):
"""Delete an iCloud Drive item."""
return self.connection.move_items_to_trash(
return self.connection.delete_items(
self.data["drivewsid"], self.data["etag"]
)

Expand All @@ -340,7 +374,7 @@ def __getitem__(self, key):
try:
return self.get(key)
except IndexError as i:
raise KeyError(f"No child named '{key}' exists") from i
raise KeyError(f"No child named {key} exists") from i

def __str__(self):
return rf"\{type: {self.type}, name: {self.name}\}"
Expand Down
Loading