/ Examples / Resource.py
Resource.py
  1  ##########################################################
  2  # This RNS example demonstrates how to transfer a        #
  3  # resource over an established link                      #
  4  ##########################################################
  5  
  6  import os
  7  import sys
  8  import time
  9  import random
 10  import argparse
 11  import RNS
 12  
 13  # Let's define an app name. We'll use this for all
 14  # destinations we create. Since this echo example
 15  # is part of a range of example utilities, we'll put
 16  # them all within the app namespace "example_utilities"
 17  APP_NAME = "example_utilities"
 18  
 19  ##########################################################
 20  #### Server Part #########################################
 21  ##########################################################
 22  
 23  # A reference to the latest client link that connected
 24  latest_client_link = None
 25  
 26  # This initialisation is executed when the users chooses
 27  # to run as a server
 28  def server(configpath):
 29      # We must first initialise Reticulum
 30      reticulum = RNS.Reticulum(configpath)
 31      
 32      # Randomly create a new identity for our link example
 33      server_identity = RNS.Identity()
 34  
 35      # We create a destination that clients can connect to. We
 36      # want clients to create links to this destination, so we
 37      # need to create a "single" destination type.
 38      server_destination = RNS.Destination(
 39          server_identity,
 40          RNS.Destination.IN,
 41          RNS.Destination.SINGLE,
 42          APP_NAME,
 43          "resourceexample"
 44      )
 45  
 46      # We configure a function that will get called every time
 47      # a new client creates a link to this destination.
 48      server_destination.set_link_established_callback(client_connected)
 49  
 50      # Everything's ready!
 51      # Let's Wait for client resources or user input
 52      server_loop(server_destination)
 53  
 54  def server_loop(destination):
 55      # Let the user know that everything is ready
 56      RNS.log(
 57          "Resource example "+
 58          RNS.prettyhexrep(destination.hash)+
 59          " running, waiting for a connection."
 60      )
 61  
 62      RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
 63  
 64      # We enter a loop that runs until the users exits.
 65      # If the user hits enter, we will announce our server
 66      # destination on the network, which will let clients
 67      # know how to create messages directed towards it.
 68      while True:
 69          entered = input()
 70          destination.announce()
 71          RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
 72  
 73  # When a client establishes a link to our server
 74  # destination, this function will be called with
 75  # a reference to the link.
 76  def client_connected(link):
 77      global latest_client_link
 78      RNS.log("Client connected")
 79  
 80      # We configure the link to accept all resources
 81      # and set a callback for completed resources
 82      link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
 83      link.set_resource_concluded_callback(resource_concluded)
 84  
 85      link.set_link_closed_callback(client_disconnected)
 86      latest_client_link = link
 87  
 88  def client_disconnected(link):
 89      RNS.log("Client disconnected")
 90  
 91  def resource_concluded(resource):
 92      if resource.status == RNS.Resource.COMPLETE:
 93          RNS.log(f"Resource {resource} received")
 94          RNS.log(f"Metadata: {resource.metadata}")
 95          RNS.log(f"Data length: {os.stat(resource.data.name).st_size}")
 96          RNS.log(f"Data can be read directly from: {resource.data}")
 97          RNS.log(f"Data can be moved or copied from: {resource.data.name}")
 98          RNS.log(f"First 32 bytes of data: {RNS.hexrep(resource.data.read(32))}")
 99      else:
100          RNS.log(f"Receiving resource {resource} failed")
101  
102  
103  
104  ##########################################################
105  #### Client Part #########################################
106  ##########################################################
107  
108  # A reference to the server link
109  server_link = None
110  
111  def random_text_generator():
112      texts = ["They looked up", "On each full moon", "Becky was upset", "I’ll stay away from it", "The pet shop stocks everything"]
113      return texts[random.randint(0, len(texts)-1)]
114  
115  # This initialisation is executed when the users chooses
116  # to run as a client
117  def client(destination_hexhash, configpath):
118      # We need a binary representation of the destination
119      # hash that was entered on the command line
120      try:
121          dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
122          if len(destination_hexhash) != dest_len:
123              raise ValueError(
124                  "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
125              )
126              
127          destination_hash = bytes.fromhex(destination_hexhash)
128      except:
129          RNS.log("Invalid destination entered. Check your input!\n")
130          sys.exit(0)
131  
132      # We must first initialise Reticulum
133      reticulum = RNS.Reticulum(configpath)
134  
135      # Check if we know a path to the destination
136      if not RNS.Transport.has_path(destination_hash):
137          RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
138          RNS.Transport.request_path(destination_hash)
139          while not RNS.Transport.has_path(destination_hash):
140              time.sleep(0.1)
141  
142      # Recall the server identity
143      server_identity = RNS.Identity.recall(destination_hash)
144  
145      # Inform the user that we'll begin connecting
146      RNS.log("Establishing link with server...")
147  
148      # When the server identity is known, we set
149      # up a destination
150      server_destination = RNS.Destination(
151          server_identity,
152          RNS.Destination.OUT,
153          RNS.Destination.SINGLE,
154          APP_NAME,
155          "resourceexample"
156      )
157  
158      # And create a link
159      link = RNS.Link(server_destination)
160  
161      # We'll set up functions to inform the
162      # user when the link is established or closed
163      link.set_link_established_callback(link_established)
164      link.set_link_closed_callback(link_closed)
165  
166      # Everything is set up, so let's enter a loop
167      # for the user to interact with the example
168      client_loop()
169  
170  def client_loop():
171      global server_link
172  
173      # Wait for the link to become active
174      while not server_link:
175          time.sleep(0.1)
176  
177      should_quit = False
178      while not should_quit:
179          try:
180              print("> ", end=" ")
181              text = input()
182  
183              # Check if we should quit the example
184              if text == "quit" or text == "q" or text == "exit":
185                  should_quit = True
186                  server_link.teardown()
187  
188              else:
189                  # Generate 32 megabytes of random data
190                  data     = os.urandom(32*1024*1024)
191                  RNS.log(f"Data length: {len(data)}")
192                  RNS.log(f"First 32 bytes of data: {RNS.hexrep(data[:32])}")
193                  
194                  # Generate some metadata
195                  metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)}
196                  
197                  # Send the resource
198                  resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
199  
200                  # Alternatively, you can stream data
201                  # directly from an open file descriptor
202  
203                  # with open("/path/to/file", "rb") as data_file:
204                  #     resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
205  
206          except Exception as e:
207              RNS.log("Error while sending resource over the link: "+str(e))
208              should_quit = True
209              server_link.teardown()
210  
211  def resource_concluded_sending(resource):
212      if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully")
213      else: RNS.log(f"Sending the resource {resource} failed")
214  
215  # This function is called when a link
216  # has been established with the server
217  def link_established(link):
218      # We store a reference to the link
219      # instance for later use
220      global server_link
221      server_link = link
222  
223      # Inform the user that the server is
224      # connected
225      RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
226  
227  # When a link is closed, we'll inform the
228  # user, and exit the program
229  def link_closed(link):
230      if link.teardown_reason == RNS.Link.TIMEOUT:
231          RNS.log("The link timed out, exiting now")
232      elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
233          RNS.log("The link was closed by the server, exiting now")
234      else:
235          RNS.log("Link closed, exiting now")
236      
237      time.sleep(1.5)
238      sys.exit(0)
239  
240  
241  ##########################################################
242  #### Program Startup #####################################
243  ##########################################################
244  
245  # This part of the program runs at startup,
246  # and parses input of from the user, and then
247  # starts up the desired program mode.
248  if __name__ == "__main__":
249      try:
250          parser = argparse.ArgumentParser(description="Simple resource example")
251  
252          parser.add_argument(
253              "-s",
254              "--server",
255              action="store_true",
256              help="wait for incoming resources from clients"
257          )
258  
259          parser.add_argument(
260              "--config",
261              action="store",
262              default=None,
263              help="path to alternative Reticulum config directory",
264              type=str
265          )
266  
267          parser.add_argument(
268              "destination",
269              nargs="?",
270              default=None,
271              help="hexadecimal hash of the server destination",
272              type=str
273          )
274  
275          args = parser.parse_args()
276  
277          if args.config:
278              configarg = args.config
279          else:
280              configarg = None
281  
282          if args.server:
283              server(configarg)
284          else:
285              if (args.destination == None):
286                  print("")
287                  parser.print_help()
288                  print("")
289              else:
290                  client(args.destination, configarg)
291  
292      except KeyboardInterrupt:
293          print("")
294          sys.exit(0)