Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest]
python-version: [3.11, 3.13]
fail-fast: true
defaults:
Expand All @@ -39,6 +39,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.10.11"
python-version: ${{ matrix.python-version }}

- name: Install dependencies
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: ruff lint
uses: astral-sh/ruff-action@v3
with:
version: "0.15.6"

- name: ruff format check
uses: astral-sh/ruff-action@v3
with:
args: "format --check"
version: "0.15.6"
args:
"format --check --diff"
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-illegal-windows-names
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
Expand All @@ -20,7 +22,7 @@ repos:
files: ^(.*\.toml)$

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.0
rev: v0.15.6
hooks:
- id: ruff
args: [ --fix ]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ dict(fs.search("/tests/logo.png"))
## Transfer.it

> [!NOTE]
> The `transfer.it` client does not support uploads (yet!)
> The `transfer.it` client does not support uploads yet (PRs welcome)

```python
from mega.transfer_it import TransferItClient
Expand Down
20 changes: 7 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,17 @@ license = "Apache-2.0"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">=3.11"
version = "2.0.3"
version = "2.1.0"

[project.optional-dependencies]
cli = [
"async-mega-py[default]",
"typer-slim>=0.21.1"
]
default = [
"python-dotenv>=1.2.1",
"rich>=14.0.0"
"cyclopts>=4.10.1",
"python-dotenv>=1.2.1"
]

[project.scripts]
async-mega-py = "mega.cli:main"
mega-py = "mega.cli:main"
async-mega-py = "mega.__main__:app.meta"
mega-py = "mega.__main__:app.meta"

[project.urls]
Homepage = "https://github.com/NTFSvolume/mega.py"
Expand Down Expand Up @@ -123,14 +119,12 @@ module-name = "mega"

[build-system]
build-backend = "uv_build"
requires = ["uv_build>=0.8.17,<0.9.0"]
requires = ["uv_build<=0.10.11"]

[dependency-groups]
dev = [
"prek>=0.3.6",
"pytest-asyncio>=0.25.0",
"pytest>=8.3.5",
"rich>=14.0.0",
"ruff>=0.15.0",
"typer-slim>=0.21.1"
"ruff==0.15.6"
]
152 changes: 150 additions & 2 deletions src/mega/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,152 @@
from mega.cli import main
from __future__ import annotations

import contextlib
import json
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Annotated

import yarl
from cyclopts import App, Parameter

from mega import __version__, env
from mega.api import LOG_HTTP_TRAFFIC
from mega.client import MegaNzClient
from mega.transfer_it import TransferItClient
from mega.utils import Site, setup_logger

if TYPE_CHECKING:
from collections.abc import AsyncGenerator


logger = logging.getLogger("mega")
CWD = Path.cwd()

app = App(
help=(
"CLI app for the [bold black]Mega.nz[/bold black] and [bold black]Transfer.it[/bold black].\n"
f"Set [bold green]{env.EMAIL.name}[/bold green] and [bold green]{env.PASSWORD.name}[/bold green]\n"
"enviroment variables to use them as credentials for Mega"
),
help_format="rich",
version=__version__,
)


@app.meta.default
def verbose(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
verbose: Annotated[
int,
Parameter(
name=["-v", "--verbose"],
count=True,
help="Increase verbosity (-v shows debug logs, -vv shows HTTP traffic)",
),
] = 0,
) -> None:
if verbose > 1:
LOG_HTTP_TRAFFIC.set(True)

level = logging.DEBUG if verbose else logging.INFO
setup_logger(level)
app(tokens)


@contextlib.asynccontextmanager
async def connect() -> AsyncGenerator[MegaNzClient]:
async with MegaNzClient() as mega:
await mega.login(env.EMAIL, env.PASSWORD)
with mega.progress_bar:
yield mega


async def transfer_it(url: str, output_dir: Path) -> None:
async with TransferItClient() as client:
with client.progress_bar:
transfer_id = client.parse_url(url)
logger.info(f"Downloading '{url}'")
results = await client.download_transfer(transfer_id, output_dir)
logger.info(
f"Download of '{url}' finished. Successful = {len(results.success)}, failed = {len(results.fails)}"
)


@app.command()
async def download(url: str, output_dir: Path = CWD) -> None:
"""Download a public file or folder by its URL (transfer.it / mega.nz)"""

site = Site(yarl.URL(url).origin())
if site is Site.TRANSFER_IT:
return await transfer_it(url, output_dir)

async with connect() as mega:
parsed_url = mega.parse_url(url)
if parsed_url.is_folder:
await download_folder(mega, url, output_dir)
else:
await download_file(mega, url, output_dir)


@app.command()
async def dump(output_dir: Path = CWD) -> None:
"""Dump a copy of your filesystem to disk"""

async with connect() as mega:
fs = await mega.get_filesystem()
out = output_dir / "filesystem.json"
out.parent.mkdir(exist_ok=True)
logger.info(f"Creating filesystem dump at '{out!s}'")
out.write_text(json.dumps(fs.dump(), indent=2, ensure_ascii=False))


@app.command()
async def stats() -> None:
"""Show account stats"""

async with connect() as mega:
stats = await mega.get_account_stats()
logger.info(f"Account stats for {env.EMAIL or 'TEMP ACCOUNT'}:")
logger.info(stats.storage.dump())
logger.info(stats.balance.dump())
fs = await mega.get_filesystem()
metrics = {root.attributes.name: stats.metrics[root.id] for root in (fs.root, fs.inbox, fs.trash_bin)}
logger.info(metrics)


@app.command()
async def upload(file_path: Path) -> None:
"""Upload a file to your account"""

async with connect() as mega:
if not env.EMAIL:
logger.warning("Files uploaded by a temp account can not be exported")

folder = await mega.create_folder("uploaded by mega.py")
logger.info(f'Uploading "{file_path!s}"')
file = await mega.upload(file_path, folder.id)
path = (await mega.get_filesystem()).absolute_path(file.id)
logger.info(f'File uploaded to your cloud. Path = "{path}"')
if not env.EMAIL:
return

link = await mega.export(file)
logger.info(f'Public link for "{file_path!s}": {link}')


async def download_file(mega: MegaNzClient, url: str, output: Path) -> None:
public_handle, public_key = mega.parse_file_url(url)
logger.info(f"Downloading {url}")
path = await mega.download_public_file(public_handle, public_key, output)
logger.info(f'Download of {url} finished. File save at "{path!s}"')


async def download_folder(mega: MegaNzClient, url: str, output: Path) -> None:
public_handle, public_key, root_node = mega.parse_folder_url(url)
logger.info(f"Downloading {url}")
results = await mega.download_public_folder(public_handle, public_key, output, root_node)
logger.info(f"Download of '{url}' finished. Successful = {len(results.success)}, failed = {len(results.fails)}")


if __name__ == "__main__":
main()
app.meta()
Loading
Loading