diff --git a/puller_fits.py b/puller_fits.py
index a05c239..78c99e4 100644
--- a/puller_fits.py
+++ b/puller_fits.py
@@ -8,81 +8,112 @@ import datetime
import json
from threading import Thread
import queue
+import math
+from functools import partial
import tqdm
directory_url = r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/"
stored_images_dir = r"..\Data"
-# directory_url = r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/suvi-l2-ci094/"
-# stored_images_dir = r"Z:\NOAA GOES Data\Data\goes16\l2\suvi-l2-ci094"
+# directory_url = r"https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes16/l2/data/ephe-l2-orb1m/2017/12/"
+# stored_images_dir = r"Z:\NOAA GOES Data\Data\goes16/l2/data/ephe-l2-orb1m/2017/12/"
ignore_folder_names = ["l1b", "goes17", "2017", "2018", "2019", "2020", "2021", "2022", "2023"]
file_database_path = r"..\file_database.json"
fetch_interval = 0 # 60*60
-nworkers = 8
+nfetchworkers = 10 # Be nice to the servers, this value is how many threads will be asking for links and file info at the same time
+ndownloadworkers = 4 # Be nice to the servers, this value is how many threads will be downloading files at the same time
randomize_order = False
-def recursive_find_links(url, attempt_count = 3, randomize_order = True):
- links_regex_pattern = r'(?<=.*\d{4}-\d{2}-\d{2} \d{2}:\d{2})' # Find href links that do not contain question marks or whitespace
- times_regex_pattern = r'(?<=
)(\d{4}-\d{2}-\d{2} \d{2}:\d{2})(?= )' # Find timestamps in UTC in the format YYYY-MM-DD HH:mm
- attempts = 0
- html_content = None
- while attempts < attempt_count:
- try:
- with urllib.request.urlopen(url) as response:
- html_content = response.read().decode('utf-8')
- break
- except Exception as e:
- print(f"Exception while fetching links: {e}")
- time.sleep(1 + random.random())
- attempts += 1
- if (attempt_count > 1) and (attempts == attempt_count):
- print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
- return
+links_regex_pattern = r'(?<=.*\d{4}-\d{2}-\d{2} \d{2}:\d{2})' # Find href links that do not contain question marks or whitespace
+times_regex_pattern = r'(?<= | )(\d{4}-\d{2}-\d{2} \d{2}:\d{2})(?= )' # Find timestamps in UTC in the format YYYY-MM-DD HH:mm
+sizes_regex_pattern = r'(?<=\d{4}-\d{2}-\d{2} \d{2}:\d{2} <\/td> | )([ .\d]{1,3})([KMG]|- |0 | )(?=<\/td>)' # Find file size markers in one of many possible formats
+links_matcher = re.compile(links_regex_pattern)
+times_matcher = re.compile(times_regex_pattern)
+sizes_matcher = re.compile(sizes_regex_pattern)
- links = re.findall(links_regex_pattern, html_content)
- times = re.findall(times_regex_pattern, html_content)
- for i in range(len(times)):
- dt = datetime.datetime.strptime(times[i].strip(), "%Y-%m-%d %H:%M")
- dt.replace(tzinfo=datetime.timezone.utc)
- times[i] = time.mktime(dt.timetuple())
-
- if len(links) - 1 == len(times):
- links = links[1:]
- elif len(links) == len(times):
- pass
- else:
- raise(ValueError)
-
- results = list(zip(links, times))
- if randomize_order:
- random.shuffle(results)
- for _link, _time in results:
- if _link.endswith("/"):
- if _link.split(r"/")[-2] in ignore_folder_names:
- continue
- else:
- yield from recursive_find_links(urllib.parse.urljoin(url,_link), attempt_count, randomize_order)
- else:
- yield urllib.parse.urljoin(url,_link), _time
-
-
-# Fetch image from url and store it to path, retrying on failure
-def image_fetch_worker(work_queue, result_queue, attempt_count = 1):
+def find_links_worker(query_work_queue, query_result_queue, attempt_count = 3, randomize_order = True):
while True:
- job = work_queue.get()
+ job = query_work_queue.get()
if job == None:
return
- url, path, t = job
+ url= job
+
+ attempts = 0
+ html_content = None
+ while attempts < attempt_count:
+ try:
+ with urllib.request.urlopen(url) as response:
+ html_content = response.read().decode('utf-8')
+ break
+ except Exception as e:
+ print(f"Exception while fetching links: {e}")
+ time.sleep(1 + random.random())
+ attempts += 1
+ if (attempt_count > 1) and (attempts == attempt_count):
+ print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
+ return
+
+ links = links_matcher.findall(html_content)
+ times = times_matcher.findall(html_content)
+ sizes = sizes_matcher.findall(html_content)
+ for i in range(len(times)):
+ dt = datetime.datetime.strptime(times[i].strip(), "%Y-%m-%d %H:%M")
+ dt.replace(tzinfo=datetime.timezone.utc)
+ times[i] = time.mktime(dt.timetuple())
+ for i in range(len(sizes)):
+ match sizes[i][1].strip():
+ case '-':
+ sizes[i] = 0
+ case 'K':
+ sizes[i] = int(float(sizes[i][0].strip()) * 1024)
+ case 'M':
+ sizes[i] = int(float(sizes[i][0].strip()) * 1024 * 1024)
+ case 'G':
+ sizes[i] = int(float(sizes[i][0].strip()) * 1024 * 1024 * 1024)
+ case "0":
+ sizes[i] = 0
+ case "":
+ sizes[i] = int(float(sizes[i][0].strip()))
+ case _:
+ raise(ValueError(f"Unexpected symbol while parsing links page: {_}"))
+
+
+ if (len(links) != len(times)) or (len(times) != len(sizes)):
+ raise(ValueError("Links parsing error, mismatched numbers of links, times, or sizes!"))
+
+ results = list(zip(links, times, sizes))
+ if randomize_order:
+ random.shuffle(results)
+ for _link, _time, _size in results:
+ if _link.endswith("/"):
+ if _link.split(r"/")[-2] in ignore_folder_names:
+ continue
+ else:
+ query_work_queue.put(urllib.parse.urljoin(url,_link))
+ else:
+ query_result_queue.put((urllib.parse.urljoin(url,_link), _time, _size))
+
+
+# Fetch file from url and store it to path, retrying on failure
+def file_download_worker(download_work_queue, download_result_queue, attempt_count = 2):
+ while True:
+ job = download_work_queue.get()
+ if job == None:
+ return
+ url, path, t, s = job
attempts = 0
while attempts < attempt_count:
try:
req = urllib.request.Request(url, data=None)
image_data = urllib.request.urlopen(req).read()
- os.makedirs(os.path.split(path)[0], exist_ok=True)
- open(path, 'wb').write(image_data)
- result_queue.put((True, url, t))
- break
+ if math.isclose(len(image_data), s, rel_tol=0.05):
+ os.makedirs(os.path.split(path)[0], exist_ok=True)
+ open(path, 'wb').write(image_data)
+ download_result_queue.put((True, url, t))
+ break
+ else:
+ raise ValueError("Downloaded file is the wrong size!")
except Exception as e:
if hasattr(e, "code") and e.code == 404: # This is expected if the file has been removed from the site (at least for swpc.noaa.gov)
attempts += attempt_count
@@ -92,7 +123,7 @@ def image_fetch_worker(work_queue, result_queue, attempt_count = 1):
attempts += 1
if (attempt_count > 1) and (attempts == attempt_count):
print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
- result_queue.put((False, url, t))
+ download_result_queue.put((False, url, t))
break
@@ -107,24 +138,33 @@ if __name__ == "__main__":
print(f"Load failed, starting with empty cache")
file_info_cache = {}
- work_queue = queue.Queue(maxsize=nworkers)
- result_queue = queue.Queue()
+ query_work_queue = queue.Queue()
+ query_result_queue = queue.Queue()
+ download_work_queue = queue.Queue()
+ download_result_queue = queue.Queue()
workers = []
- for _ in range(nworkers):
- t = Thread(target=image_fetch_worker, args=(work_queue, result_queue), daemon=True)
+ for _ in range(nfetchworkers):
+ t = Thread(target=find_links_worker, args=(query_work_queue, query_result_queue, 3, randomize_order), daemon=True)
t.start()
workers.append(t)
+ for _ in range(ndownloadworkers):
+ t = Thread(target=file_download_worker, args=(download_work_queue, download_result_queue), daemon=True)
+ t.start()
+ workers.append(t)
+
try:
while True:
fetched_image_count = 0
already_had_image_count = 0
failed_image_count = 0
urllen = len(directory_url)
- for l, t in tqdm.tqdm(recursive_find_links(directory_url, randomize_order = randomize_order), desc="Downloading files"):
+
+ query_work_queue.put(directory_url)
+ for l, t, s in tqdm.tqdm(iter(partial(query_result_queue.get, timeout=30.0), None), desc="Downloading files"):
# Collect completed jobs and record completion status
while True:
try:
- r_success, r_url, r_t = result_queue.get_nowait()
+ r_success, r_url, r_t = download_result_queue.get_nowait()
if r_success:
fetched_image_count += 1
file_info_cache[r_url] = r_t
@@ -155,11 +195,15 @@ if __name__ == "__main__":
if l in file_info_cache: # If we have record of this file, it must be out of date, delete it and download the new version.
if os.path.exists(filepath):
os.remove(filepath)
- else: # If we have no record of this file, update the file info cache and don't redownload
- file_info_cache[l] = t
- already_had_image_count += 1
- continue
- work_queue.put((l, filepath, t))
+ else: # If we have no record of this file, update the file info cache and don't redownload if fsize is right
+ fsize = os.path.getsize(filepath)
+ if math.isclose(fsize, s, rel_tol=0.05):
+ file_info_cache[l] = t
+ already_had_image_count += 1
+ continue
+ else:
+ print(f'Found a mismatched size on file: {filepath} Redownloading!')
+ download_work_queue.put((l, filepath, t, s))
else:
# We have a download record, confirm the file actually exists on disk
if os.path.exists(filepath):
@@ -170,7 +214,7 @@ if __name__ == "__main__":
if os.path.exists(filepath2) or os.path.exists(filepath3):
already_had_image_count += 1
else: # We could not find the file on disk, queue for redownload
- work_queue.put((l, filepath, t))
+ download_work_queue.put((l, filepath, t))
with open(file_database_path, 'w') as f:
f.write(json.dumps(file_info_cache))
@@ -185,10 +229,19 @@ if __name__ == "__main__":
break
except KeyboardInterrupt:
print("Saving file database and shutting down.")
+ except Exception as e:
+ print(f"Unhandled Exception during run: {e}")
+ print("Shutting down")
- for _ in range(nworkers):
+ for _ in range(nfetchworkers):
try:
- work_queue.put(None, timeout=5.0)
+ query_work_queue.put(None, timeout=5.0)
+ except:
+ break
+
+ for _ in range(ndownloadworkers):
+ try:
+ download_work_queue.put(None, timeout=5.0)
except:
break
|