hippius 0.2.5__tar.gz → 0.2.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hippius
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Python SDK and CLI for Hippius blockchain storage
5
5
  Home-page: https://github.com/thenervelab/hippius-sdk
6
6
  Author: Dubs
@@ -26,7 +26,7 @@ from hippius_sdk.config import (
26
26
  from hippius_sdk.ipfs import IPFSClient
27
27
  from hippius_sdk.utils import format_cid, format_size, hex_to_ipfs_cid
28
28
 
29
- __version__ = "0.2.5"
29
+ __version__ = "0.2.7"
30
30
  __all__ = [
31
31
  "HippiusClient",
32
32
  "IPFSClient",
@@ -152,6 +152,7 @@ def main():
152
152
  args.file_path,
153
153
  miner_ids,
154
154
  encrypt=encrypt,
155
+ publish=not args.no_publish if hasattr(args, "no_publish") else True,
155
156
  )
156
157
 
157
158
  elif args.command == "store-dir":
@@ -235,6 +236,15 @@ def main():
235
236
  force=args.force if hasattr(args, "force") else False,
236
237
  )
237
238
 
239
+ elif args.command == "pin":
240
+ return run_async_handler(
241
+ cli_handlers.handle_pin,
242
+ client,
243
+ args.cid,
244
+ publish=not args.no_publish if hasattr(args, "no_publish") else True,
245
+ miner_ids=miner_ids,
246
+ )
247
+
238
248
  elif args.command == "ec-delete":
239
249
  return run_async_handler(
240
250
  cli_handlers.handle_ec_delete,
@@ -51,6 +51,7 @@ from hippius_sdk.errors import (
51
51
  HippiusFailedSubstrateDelete,
52
52
  HippiusMetadataError,
53
53
  )
54
+ from hippius_sdk.substrate import FileInput
54
55
 
55
56
  try:
56
57
  import nacl.secret
@@ -246,6 +247,7 @@ async def handle_store(
246
247
  file_path: str,
247
248
  miner_ids: Optional[List[str]] = None,
248
249
  encrypt: Optional[bool] = None,
250
+ publish: bool = True,
249
251
  ) -> int:
250
252
  """Handle the store command (upload file to IPFS and store on Substrate)"""
251
253
  if not os.path.exists(file_path):
@@ -256,6 +258,16 @@ async def handle_store(
256
258
  error(f"[bold]{file_path}[/bold] is not a file")
257
259
  return 1
258
260
 
261
+ # If publishing is enabled, ensure we have a valid substrate client by accessing it
262
+ # This will trigger password prompts if needed right at the beginning
263
+ if publish and hasattr(client, "substrate_client") and client.substrate_client:
264
+ try:
265
+ # Force keypair initialization - this will prompt for password if needed
266
+ _ = client.substrate_client._ensure_keypair()
267
+ except Exception as e:
268
+ warning(f"Failed to initialize blockchain client: {str(e)}")
269
+ warning("Will continue with upload but blockchain publishing may fail")
270
+
259
271
  # Get file size for display
260
272
  file_size = os.path.getsize(file_path)
261
273
  file_name = os.path.basename(file_path)
@@ -282,6 +294,19 @@ async def handle_store(
282
294
  "[bold yellow]Encryption: Using default setting[/bold yellow]"
283
295
  )
284
296
 
297
+ # Add publishing status
298
+ if not publish:
299
+ upload_info.append(
300
+ "[bold yellow]Publishing: Disabled (local upload only)[/bold yellow]"
301
+ )
302
+ log(
303
+ "\nUpload will be local only - not publishing to blockchain or pinning to IPFS"
304
+ )
305
+ else:
306
+ upload_info.append(
307
+ "[bold green]Publishing: Enabled (publishing to blockchain)[/bold green]"
308
+ )
309
+
285
310
  # Parse miner IDs if provided
286
311
  miner_id_list = None
287
312
  if miner_ids:
@@ -318,13 +343,51 @@ async def handle_store(
318
343
  updater = asyncio.create_task(update_progress())
319
344
 
320
345
  try:
321
- # Use the store_file method
346
+ # Use the upload_file method to get the CID
322
347
  result = await client.upload_file(
323
348
  file_path=file_path,
324
349
  encrypt=encrypt,
325
- # miner_ids=miner_id_list
326
350
  )
327
351
 
352
+ # If publishing is enabled, store on blockchain
353
+ if publish and result.get("cid"):
354
+ try:
355
+ # Pin and publish the file globally
356
+ # First pin in IPFS (essential step for publishing)
357
+ await client.ipfs_client.pin(result["cid"])
358
+
359
+ # Then publish globally to make available across network
360
+ publish_result = await client.ipfs_client.publish_global(
361
+ result["cid"]
362
+ )
363
+
364
+ log(
365
+ "\n[green]File has been pinned to IPFS and published to the network[/green]"
366
+ )
367
+
368
+ # Add gateway URL to the result for use in output
369
+ if "cid" in result:
370
+ result[
371
+ "gateway_url"
372
+ ] = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
373
+
374
+ # Store on blockchain if miners are provided
375
+ if miner_ids:
376
+ # Create a file input for blockchain storage
377
+ file_input = FileInput(
378
+ file_hash=result["cid"], file_name=file_name
379
+ )
380
+
381
+ # Submit storage request
382
+ tx_hash = await client.substrate_client.storage_request(
383
+ files=[file_input], miner_ids=miner_id_list
384
+ )
385
+
386
+ # Add transaction hash to result
387
+ result["transaction_hash"] = tx_hash
388
+ except Exception as e:
389
+ warning(f"Failed to publish file globally: {str(e)}")
390
+
328
391
  progress.update(task, completed=100)
329
392
  updater.cancel()
330
393
 
@@ -336,16 +399,24 @@ async def handle_store(
336
399
  f"IPFS CID: [bold cyan]{result['cid']}[/bold cyan]",
337
400
  ]
338
401
 
339
- if result.get("gateway_url"):
340
- success_info.append(
341
- f"Gateway URL: [link]{result['gateway_url']}[/link]"
342
- )
402
+ # Always add the gateway URL
403
+ gateway_url = result.get("gateway_url")
404
+ if not gateway_url and "cid" in result:
405
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
406
+
407
+ if gateway_url:
408
+ success_info.append(f"Gateway URL: [link]{gateway_url}[/link]")
343
409
 
344
410
  if result.get("encrypted"):
345
411
  success_info.append(
346
412
  "[bold yellow]File was encrypted during upload[/bold yellow]"
347
413
  )
348
414
 
415
+ if not publish:
416
+ success_info.append(
417
+ "[bold yellow]File was uploaded locally only (not published to blockchain)[/bold yellow]"
418
+ )
419
+
349
420
  print_panel("\n".join(success_info), title="Upload Successful")
350
421
 
351
422
  # If we stored in the marketplace
@@ -382,6 +453,16 @@ async def handle_store_dir(
382
453
  error(f"[bold]{dir_path}[/bold] is not a directory")
383
454
  return 1
384
455
 
456
+ # If publishing is enabled, ensure we have a valid substrate client by accessing it
457
+ # This will trigger password prompts if needed right at the beginning
458
+ if publish and hasattr(client, "substrate_client") and client.substrate_client:
459
+ try:
460
+ # Force keypair initialization - this will prompt for password if needed
461
+ _ = client.substrate_client._ensure_keypair()
462
+ except Exception as e:
463
+ warning(f"Failed to initialize blockchain client: {str(e)}")
464
+ warning("Will continue with upload but blockchain publishing may fail")
465
+
385
466
  # Upload information panel
386
467
  upload_info = [f"Directory: [bold]{dir_path}[/bold]"]
387
468
 
@@ -460,14 +541,62 @@ async def handle_store_dir(
460
541
  del result["transaction_hash"]
461
542
  else:
462
543
  # If we want to publish, make sure files are pinned globally
463
- for file_info in result.get("files", []):
464
- if "cid" in file_info:
465
- try:
466
- await client.ipfs_client.publish_global(file_info["cid"])
467
- except Exception as e:
468
- warning(
469
- f"Failed to publish file {file_info['name']} globally: {str(e)}"
470
- )
544
+ try:
545
+ # Add gateway URL to the result for use in output
546
+ if "cid" in result:
547
+ result[
548
+ "gateway_url"
549
+ ] = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
550
+
551
+ # Pin and publish the directory root CID globally
552
+ # First pin in IPFS (essential step for publishing)
553
+ await client.ipfs_client.pin(result["cid"])
554
+
555
+ # Then publish globally to make available across network
556
+ await client.ipfs_client.publish_global(result["cid"])
557
+
558
+ log(
559
+ "\n[green]Directory has been pinned to IPFS and published to the network[/green]"
560
+ )
561
+
562
+ # Also pin and publish individual files if available
563
+ for file_info in result.get("files", []):
564
+ if "cid" in file_info:
565
+ try:
566
+ # Pin each file to ensure availability
567
+ await client.ipfs_client.pin(file_info["cid"])
568
+
569
+ # Then publish globally
570
+ await client.ipfs_client.publish_global(
571
+ file_info["cid"]
572
+ )
573
+ except Exception as e:
574
+ warning(
575
+ f"Failed to publish file {file_info['name']} globally: {str(e)}"
576
+ )
577
+
578
+ # Store on blockchain if miners are provided - this is what requires a password
579
+ if (
580
+ miner_ids
581
+ and hasattr(client, "substrate_client")
582
+ and client.substrate_client
583
+ ):
584
+ # Create a file input for blockchain storage
585
+ file_input = FileInput(
586
+ file_hash=result["cid"],
587
+ file_name=os.path.basename(dir_path),
588
+ )
589
+
590
+ # This will prompt for a password if needed
591
+ tx_hash = await client.substrate_client.storage_request(
592
+ files=[file_input], miner_ids=miner_id_list
593
+ )
594
+
595
+ # Add transaction hash to result
596
+ result["transaction_hash"] = tx_hash
597
+
598
+ except Exception as e:
599
+ warning(f"Failed to publish directory globally: {str(e)}")
471
600
 
472
601
  # Complete the progress
473
602
  progress.update(task, completed=100)
@@ -482,9 +611,23 @@ async def handle_store_dir(
482
611
  f"Directory CID: [bold cyan]{result['cid']}[/bold cyan]",
483
612
  ]
484
613
 
485
- if result.get("gateway_url"):
614
+ # Always add the gateway URL
615
+ gateway_url = result.get("gateway_url")
616
+ if not gateway_url and "cid" in result:
617
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{result['cid']}"
618
+
619
+ if gateway_url:
620
+ success_info.append(f"Gateway URL: [link]{gateway_url}[/link]")
621
+
622
+ # Add encryption and publish status to success info
623
+ if result.get("encrypted"):
624
+ success_info.append(
625
+ "[bold yellow]Directory was encrypted during upload[/bold yellow]"
626
+ )
627
+
628
+ if not publish:
486
629
  success_info.append(
487
- f"Gateway URL: [link]{result['gateway_url']}[/link]"
630
+ "[bold yellow]Directory was uploaded locally only (not published to blockchain)[/bold yellow]"
488
631
  )
489
632
 
490
633
  print_panel("\n".join(success_info), title="Directory Upload Successful")
@@ -508,10 +651,15 @@ async def handle_store_dir(
508
651
  )
509
652
 
510
653
  # If publishing is enabled and we stored in the marketplace
511
- if publish and "transaction_hash" in result:
512
- log(
513
- f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
514
- )
654
+ if publish:
655
+ # We only include transaction hash stuff if we actually created a blockchain transaction
656
+ if "transaction_hash" in result:
657
+ log(
658
+ f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
659
+ )
660
+ else:
661
+ # If publish is true but no transaction hash, just indicate files were published to IPFS
662
+ log("\n[green]Directory was published to IPFS network.[/green]")
515
663
  elif not publish:
516
664
  log(
517
665
  "\n[yellow]Files were uploaded locally only. No blockchain publication or IPFS pinning.[/yellow]"
@@ -1565,6 +1713,117 @@ async def handle_delete(client: HippiusClient, cid: str, force: bool = False) ->
1565
1713
  return 0
1566
1714
 
1567
1715
 
1716
+ async def handle_pin(
1717
+ client: HippiusClient, cid: str, publish: bool = True, miner_ids=None
1718
+ ) -> int:
1719
+ """Handle the pin command to pin a CID to IPFS and optionally publish to blockchain"""
1720
+ from rich.panel import Panel
1721
+
1722
+ # First check if this CID exists
1723
+ try:
1724
+ exists_result = await client.exists(cid)
1725
+ if not exists_result["exists"]:
1726
+ error(f"CID [bold cyan]{cid}[/bold cyan] not found on IPFS")
1727
+ return 1
1728
+ except Exception as e:
1729
+ warning(f"Error checking if CID exists: {e}")
1730
+ return 1
1731
+
1732
+ # Create operation title based on publish flag
1733
+ if publish:
1734
+ info(
1735
+ f"Preparing to pin and publish content with CID: [bold cyan]{cid}[/bold cyan]"
1736
+ )
1737
+ operation_title = "Pin & Publish Operation"
1738
+ else:
1739
+ info(f"Preparing to pin content with CID: [bold cyan]{cid}[/bold cyan]")
1740
+ operation_title = "Pin Operation"
1741
+
1742
+ # Display operation details
1743
+ operation_details = [
1744
+ f"CID: [bold cyan]{cid}[/bold cyan]",
1745
+ f"Publishing to blockchain: {'Enabled' if publish else 'Disabled'}",
1746
+ ]
1747
+ print_panel("\n".join(operation_details), title=operation_title)
1748
+
1749
+ # Need to authenticate if publishing to blockchain
1750
+ if publish:
1751
+ try:
1752
+ # Ensure we have a keypair for substrate operations
1753
+ _ = client.substrate_client._ensure_keypair()
1754
+ except Exception as e:
1755
+ warning(f"Failed to initialize blockchain client: {str(e)}")
1756
+ warning("Will continue with pinning but blockchain publishing may fail")
1757
+
1758
+ # Show spinner during pinning
1759
+ with console.status(
1760
+ "[cyan]Pinning content to IPFS...[/cyan]", spinner="dots"
1761
+ ) as status:
1762
+ try:
1763
+ # Pin the content to IPFS
1764
+ pin_result = await client.ipfs_client.pin(cid)
1765
+
1766
+ if not pin_result.get("success", False):
1767
+ error(
1768
+ f"Failed to pin content: {pin_result.get('message', 'Unknown error')}"
1769
+ )
1770
+ return 1
1771
+
1772
+ # If publishing to blockchain, do that now
1773
+ if publish:
1774
+ status.update("[cyan]Publishing content to blockchain...[/cyan]")
1775
+
1776
+ # Create a FileInput object for the substrate client
1777
+ from hippius_sdk.substrate import FileInput
1778
+
1779
+ file_input = FileInput(file_hash=cid, file_name=f"pinned_{cid}")
1780
+
1781
+ # Submit the storage request
1782
+ tx_hash = await client.substrate_client.storage_request(
1783
+ files=[file_input], miner_ids=miner_ids
1784
+ )
1785
+
1786
+ # Create result panel with blockchain details
1787
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{cid}"
1788
+ panel_details = [
1789
+ f"Successfully pinned and published: [bold cyan]{cid}[/bold cyan]",
1790
+ f"Gateway URL: [bold cyan]{gateway_url}[/bold cyan]",
1791
+ f"Transaction hash: [bold green]{tx_hash}[/bold green]",
1792
+ "\nThis content is now:",
1793
+ "1. Pinned to your IPFS node",
1794
+ "2. Published to the IPFS network",
1795
+ "3. Stored on the Hippius blockchain",
1796
+ ]
1797
+ console.print(
1798
+ Panel(
1799
+ "\n".join(panel_details),
1800
+ title="Operation Complete",
1801
+ border_style="green",
1802
+ )
1803
+ )
1804
+ else:
1805
+ # Just pinning, no blockchain publishing
1806
+ gateway_url = f"{client.ipfs_client.gateway}/ipfs/{cid}"
1807
+ panel_details = [
1808
+ f"Successfully pinned: [bold cyan]{cid}[/bold cyan]",
1809
+ f"Gateway URL: [bold cyan]{gateway_url}[/bold cyan]",
1810
+ "\nThis content is now pinned to your IPFS node.",
1811
+ "It will remain available as long as your node is running.",
1812
+ ]
1813
+ console.print(
1814
+ Panel(
1815
+ "\n".join(panel_details),
1816
+ title="Pinning Complete",
1817
+ border_style="green",
1818
+ )
1819
+ )
1820
+
1821
+ return 0
1822
+ except Exception as e:
1823
+ error(f"Error during operation: {e}")
1824
+ return 1
1825
+
1826
+
1568
1827
  async def handle_ec_delete(
1569
1828
  client: HippiusClient, metadata_cid: str, force: bool = False
1570
1829
  ) -> int:
@@ -63,13 +63,19 @@ examples:
63
63
 
64
64
  # Erasure code without publishing to global IPFS network
65
65
  hippius erasure-code large_file.avi --no-publish
66
-
66
+
67
67
  # Reconstruct an erasure-coded file
68
68
  hippius reconstruct QmMetadataHash reconstructed_file.mp4
69
-
69
+
70
+ # Pin a CID to IPFS and publish to blockchain
71
+ hippius pin QmHash
72
+
73
+ # Pin a CID to IPFS without publishing to blockchain
74
+ hippius pin QmHash --no-publish
75
+
70
76
  # Delete a file from IPFS and marketplace
71
77
  hippius delete QmHash
72
-
78
+
73
79
  # Delete an erasure-coded file and all its chunks
74
80
  hippius ec-delete QmMetadataHash
75
81
  """,
@@ -250,6 +256,23 @@ def add_storage_commands(subparsers):
250
256
  help="Delete without confirmation prompt",
251
257
  )
252
258
 
259
+ # Pin command
260
+ pin_parser = subparsers.add_parser(
261
+ "pin",
262
+ help="Pin a CID to IPFS and publish to blockchain",
263
+ )
264
+ pin_parser.add_argument("cid", help="CID to pin")
265
+ pin_parser.add_argument(
266
+ "--publish",
267
+ action="store_true",
268
+ help="Publish file to IPFS and store on the blockchain (default)",
269
+ )
270
+ pin_parser.add_argument(
271
+ "--no-publish",
272
+ action="store_true",
273
+ help="Don't publish file to blockchain (local pinning only)",
274
+ )
275
+
253
276
  # Keygen command
254
277
  keygen_parser = subparsers.add_parser(
255
278
  "keygen", help="Generate an encryption key for secure file storage"
@@ -443,6 +443,7 @@ class IPFSClient:
443
443
  output_path: str,
444
444
  decrypt: Optional[bool] = None,
445
445
  max_retries: int = 3,
446
+ skip_directory_check: bool = False,
446
447
  ) -> Dict[str, Any]:
447
448
  """
448
449
  Download a file from IPFS with optional decryption.
@@ -453,6 +454,7 @@ class IPFSClient:
453
454
  output_path: Path where the downloaded file/directory will be saved
454
455
  decrypt: Whether to decrypt the file (overrides default)
455
456
  max_retries: Maximum number of retry attempts (default: 3)
457
+ skip_directory_check: If True, skips directory check (treats as file)
456
458
 
457
459
  Returns:
458
460
  Dict[str, Any]: Dictionary containing download results:
@@ -470,15 +472,17 @@ class IPFSClient:
470
472
  """
471
473
  start_time = time.time()
472
474
 
473
- # Use the improved ls function to properly detect directories
475
+ # Skip directory check if requested (important for erasure code chunks)
474
476
  is_directory = False
475
- try:
476
- # The ls function now properly detects directories
477
- ls_result = await self.client.ls(cid)
478
- is_directory = ls_result.get("is_directory", False)
479
- except Exception:
480
- # If ls fails, we'll proceed as if it's a file
481
- pass
477
+ if not skip_directory_check:
478
+ # Use the improved ls function to properly detect directories
479
+ try:
480
+ # The ls function now properly detects directories
481
+ ls_result = await self.client.ls(cid)
482
+ is_directory = ls_result.get("is_directory", False)
483
+ except Exception:
484
+ # If ls fails, we'll proceed as if it's a file
485
+ pass
482
486
 
483
487
  # If it's a directory, handle it differently
484
488
  if is_directory:
@@ -530,7 +534,10 @@ class IPFSClient:
530
534
  else:
531
535
  download_path = output_path
532
536
 
533
- await self.client.download_file(cid, download_path)
537
+ # Pass the skip_directory_check parameter to the core client
538
+ await self.client.download_file(
539
+ cid, download_path, skip_directory_check=skip_directory_check
540
+ )
534
541
  download_success = True
535
542
 
536
543
  if not download_success:
@@ -1225,8 +1232,12 @@ class IPFSClient:
1225
1232
  async def download_chunk(cid, path, chunk_info):
1226
1233
  async with encoded_chunks_semaphore:
1227
1234
  try:
1235
+ # Always skip directory check for erasure code chunks
1228
1236
  await self.download_file(
1229
- cid, path, max_retries=max_retries
1237
+ cid,
1238
+ path,
1239
+ max_retries=max_retries,
1240
+ skip_directory_check=True,
1230
1241
  )
1231
1242
 
1232
1243
  # Read chunk data
@@ -219,7 +219,9 @@ class AsyncIPFSClient:
219
219
  except httpx.HTTPError:
220
220
  return False
221
221
 
222
- async def download_file(self, cid: str, output_path: str) -> str:
222
+ async def download_file(
223
+ self, cid: str, output_path: str, skip_directory_check: bool = False
224
+ ) -> str:
223
225
  """
224
226
  Download content from IPFS to a file.
225
227
  If the CID is a directory, it will create a directory and download all files.
@@ -227,19 +229,22 @@ class AsyncIPFSClient:
227
229
  Args:
228
230
  cid: Content identifier
229
231
  output_path: Path where to save the file/directory
232
+ skip_directory_check: If True, skip directory check (useful for erasure code chunks)
230
233
 
231
234
  Returns:
232
235
  Path to the saved file/directory
233
236
  """
234
- # First, check if this is a directory using the improved ls function
235
- try:
236
- ls_result = await self.ls(cid)
237
- if ls_result.get("is_directory", False):
238
- # It's a directory, use the get command to download it properly
239
- return await self.download_directory_with_get(cid, output_path)
240
- except Exception:
241
- # If ls check fails, continue with regular file download
242
- pass
237
+ # Skip directory check if requested (useful for erasure code chunks)
238
+ if not skip_directory_check:
239
+ # First, check if this is a directory using the improved ls function
240
+ try:
241
+ ls_result = await self.ls(cid)
242
+ if ls_result.get("is_directory", False):
243
+ # It's a directory, use the get command to download it properly
244
+ return await self.download_directory_with_get(cid, output_path)
245
+ except Exception:
246
+ # If ls check fails, continue with regular file download
247
+ pass
243
248
 
244
249
  # If we reached here, treat it as a regular file
245
250
  try:
@@ -251,13 +256,14 @@ class AsyncIPFSClient:
251
256
  f.write(content)
252
257
  return output_path
253
258
  except Exception as e:
254
- # As a last resort, try using the get command anyway
255
- # This is helpful if the CID is a directory but we failed to detect it
256
- try:
257
- return await self.download_directory_with_get(cid, output_path)
258
- except Exception:
259
- # If all methods fail, re-raise the original error
260
- raise e
259
+ # Only try directory fallback if not skipping directory check
260
+ if not skip_directory_check:
261
+ try:
262
+ return await self.download_directory_with_get(cid, output_path)
263
+ except Exception:
264
+ pass
265
+ # Raise the original error
266
+ raise e
261
267
 
262
268
  async def download_directory(self, cid: str, output_path: str) -> str:
263
269
  """
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hippius"
3
- version = "0.2.5"
3
+ version = "0.2.7"
4
4
  description = "Python SDK and CLI for Hippius blockchain storage"
5
5
  authors = ["Dubs <dubs@dubs.rs>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes