archivebox_binary.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python3
  2. """
  3. archivebox binary <action> [args...] [--filters]
  4. Manage Binary records (detected executables like chrome, wget, etc.).
  5. Actions:
  6. create - Create/register a Binary
  7. list - List Binaries as JSONL (with optional filters)
  8. update - Update Binaries from stdin JSONL
  9. delete - Delete Binaries from stdin JSONL
  10. Examples:
  11. # List all binaries
  12. archivebox binary list
  13. # List specific binary
  14. archivebox binary list --name=chrome
  15. # List binaries with specific version
  16. archivebox binary list --version__icontains=120
  17. # Delete old binary entries
  18. archivebox binary list --name=chrome | archivebox binary delete --yes
  19. """
  20. __package__ = 'archivebox.cli'
  21. __command__ = 'archivebox binary'
  22. import sys
  23. from typing import Optional
  24. import rich_click as click
  25. from rich import print as rprint
  26. from archivebox.cli.cli_utils import apply_filters
  27. # =============================================================================
  28. # CREATE
  29. # =============================================================================
  30. def create_binary(
  31. name: str,
  32. abspath: str,
  33. version: str = '',
  34. ) -> int:
  35. """
  36. Create/register a Binary.
  37. Exit codes:
  38. 0: Success
  39. 1: Failure
  40. """
  41. from archivebox.misc.jsonl import write_record
  42. from archivebox.machine.models import Binary
  43. is_tty = sys.stdout.isatty()
  44. if not name or not abspath:
  45. rprint('[red]Both --name and --abspath are required[/red]', file=sys.stderr)
  46. return 1
  47. try:
  48. binary, created = Binary.objects.get_or_create(
  49. name=name,
  50. abspath=abspath,
  51. defaults={'version': version}
  52. )
  53. if not is_tty:
  54. write_record(binary.to_json())
  55. if created:
  56. rprint(f'[green]Created binary: {name} at {abspath}[/green]', file=sys.stderr)
  57. else:
  58. rprint(f'[dim]Binary already exists: {name} at {abspath}[/dim]', file=sys.stderr)
  59. return 0
  60. except Exception as e:
  61. rprint(f'[red]Error creating binary: {e}[/red]', file=sys.stderr)
  62. return 1
  63. # =============================================================================
  64. # LIST
  65. # =============================================================================
  66. def list_binaries(
  67. name: Optional[str] = None,
  68. abspath__icontains: Optional[str] = None,
  69. version__icontains: Optional[str] = None,
  70. limit: Optional[int] = None,
  71. ) -> int:
  72. """
  73. List Binaries as JSONL with optional filters.
  74. Exit codes:
  75. 0: Success (even if no results)
  76. """
  77. from archivebox.misc.jsonl import write_record
  78. from archivebox.machine.models import Binary
  79. is_tty = sys.stdout.isatty()
  80. queryset = Binary.objects.all().order_by('name', '-loaded_at')
  81. # Apply filters
  82. filter_kwargs = {
  83. 'name': name,
  84. 'abspath__icontains': abspath__icontains,
  85. 'version__icontains': version__icontains,
  86. }
  87. queryset = apply_filters(queryset, filter_kwargs, limit=limit)
  88. count = 0
  89. for binary in queryset:
  90. if is_tty:
  91. rprint(f'[cyan]{binary.name:20}[/cyan] [dim]{binary.version:15}[/dim] {binary.abspath}')
  92. else:
  93. write_record(binary.to_json())
  94. count += 1
  95. rprint(f'[dim]Listed {count} binaries[/dim]', file=sys.stderr)
  96. return 0
  97. # =============================================================================
  98. # UPDATE
  99. # =============================================================================
  100. def update_binaries(
  101. version: Optional[str] = None,
  102. abspath: Optional[str] = None,
  103. ) -> int:
  104. """
  105. Update Binaries from stdin JSONL.
  106. Reads Binary records from stdin and applies updates.
  107. Uses PATCH semantics - only specified fields are updated.
  108. Exit codes:
  109. 0: Success
  110. 1: No input or error
  111. """
  112. from archivebox.misc.jsonl import read_stdin, write_record
  113. from archivebox.machine.models import Binary
  114. is_tty = sys.stdout.isatty()
  115. records = list(read_stdin())
  116. if not records:
  117. rprint('[yellow]No records provided via stdin[/yellow]', file=sys.stderr)
  118. return 1
  119. updated_count = 0
  120. for record in records:
  121. binary_id = record.get('id')
  122. if not binary_id:
  123. continue
  124. try:
  125. binary = Binary.objects.get(id=binary_id)
  126. # Apply updates from CLI flags
  127. if version:
  128. binary.version = version
  129. if abspath:
  130. binary.abspath = abspath
  131. binary.save()
  132. updated_count += 1
  133. if not is_tty:
  134. write_record(binary.to_json())
  135. except Binary.DoesNotExist:
  136. rprint(f'[yellow]Binary not found: {binary_id}[/yellow]', file=sys.stderr)
  137. continue
  138. rprint(f'[green]Updated {updated_count} binaries[/green]', file=sys.stderr)
  139. return 0
  140. # =============================================================================
  141. # DELETE
  142. # =============================================================================
  143. def delete_binaries(yes: bool = False, dry_run: bool = False) -> int:
  144. """
  145. Delete Binaries from stdin JSONL.
  146. Requires --yes flag to confirm deletion.
  147. Exit codes:
  148. 0: Success
  149. 1: No input or missing --yes flag
  150. """
  151. from archivebox.misc.jsonl import read_stdin
  152. from archivebox.machine.models import Binary
  153. records = list(read_stdin())
  154. if not records:
  155. rprint('[yellow]No records provided via stdin[/yellow]', file=sys.stderr)
  156. return 1
  157. binary_ids = [r.get('id') for r in records if r.get('id')]
  158. if not binary_ids:
  159. rprint('[yellow]No valid binary IDs in input[/yellow]', file=sys.stderr)
  160. return 1
  161. binaries = Binary.objects.filter(id__in=binary_ids)
  162. count = binaries.count()
  163. if count == 0:
  164. rprint('[yellow]No matching binaries found[/yellow]', file=sys.stderr)
  165. return 0
  166. if dry_run:
  167. rprint(f'[yellow]Would delete {count} binaries (dry run)[/yellow]', file=sys.stderr)
  168. for binary in binaries:
  169. rprint(f' {binary.name} {binary.abspath}', file=sys.stderr)
  170. return 0
  171. if not yes:
  172. rprint('[red]Use --yes to confirm deletion[/red]', file=sys.stderr)
  173. return 1
  174. # Perform deletion
  175. deleted_count, _ = binaries.delete()
  176. rprint(f'[green]Deleted {deleted_count} binaries[/green]', file=sys.stderr)
  177. return 0
  178. # =============================================================================
  179. # CLI Commands
  180. # =============================================================================
  181. @click.group()
  182. def main():
  183. """Manage Binary records (detected executables)."""
  184. pass
  185. @main.command('create')
  186. @click.option('--name', '-n', required=True, help='Binary name (e.g., chrome, wget)')
  187. @click.option('--abspath', '-p', required=True, help='Absolute path to binary')
  188. @click.option('--version', '-v', default='', help='Binary version')
  189. def create_cmd(name: str, abspath: str, version: str):
  190. """Create/register a Binary."""
  191. sys.exit(create_binary(name=name, abspath=abspath, version=version))
  192. @main.command('list')
  193. @click.option('--name', '-n', help='Filter by name')
  194. @click.option('--abspath__icontains', help='Filter by path contains')
  195. @click.option('--version__icontains', help='Filter by version contains')
  196. @click.option('--limit', type=int, help='Limit number of results')
  197. def list_cmd(name: Optional[str], abspath__icontains: Optional[str],
  198. version__icontains: Optional[str], limit: Optional[int]):
  199. """List Binaries as JSONL."""
  200. sys.exit(list_binaries(
  201. name=name,
  202. abspath__icontains=abspath__icontains,
  203. version__icontains=version__icontains,
  204. limit=limit,
  205. ))
  206. @main.command('update')
  207. @click.option('--version', '-v', help='Set version')
  208. @click.option('--abspath', '-p', help='Set path')
  209. def update_cmd(version: Optional[str], abspath: Optional[str]):
  210. """Update Binaries from stdin JSONL."""
  211. sys.exit(update_binaries(version=version, abspath=abspath))
  212. @main.command('delete')
  213. @click.option('--yes', '-y', is_flag=True, help='Confirm deletion')
  214. @click.option('--dry-run', is_flag=True, help='Show what would be deleted')
  215. def delete_cmd(yes: bool, dry_run: bool):
  216. """Delete Binaries from stdin JSONL."""
  217. sys.exit(delete_binaries(yes=yes, dry_run=dry_run))
  218. if __name__ == '__main__':
  219. main()