2017-07-04 10:38:07 +00:00
|
|
|
import os
|
2017-10-18 22:38:17 +00:00
|
|
|
import json
|
|
|
|
|
2017-07-04 10:38:07 +00:00
|
|
|
from datetime import datetime
|
2017-07-05 09:59:09 +00:00
|
|
|
from string import Template
|
2019-03-31 01:29:16 +00:00
|
|
|
from typing import List, Tuple, Iterator, Optional, Mapping
|
2019-03-26 07:20:41 +00:00
|
|
|
|
2019-04-02 20:36:41 +00:00
|
|
|
from core.schema import Link, ArchiveResult
|
|
|
|
from core.config import (
|
2019-02-21 22:45:28 +00:00
|
|
|
OUTPUT_DIR,
|
2018-06-11 01:12:55 +00:00
|
|
|
TEMPLATES_DIR,
|
2019-03-27 19:35:13 +00:00
|
|
|
VERSION,
|
2017-10-18 22:38:17 +00:00
|
|
|
GIT_SHA,
|
2018-04-17 13:13:38 +00:00
|
|
|
FOOTER_INFO,
|
2019-03-27 14:31:22 +00:00
|
|
|
TIMEOUT,
|
2017-07-05 21:33:51 +00:00
|
|
|
)
|
2019-04-02 20:36:41 +00:00
|
|
|
from core.util import (
|
|
|
|
ts_to_date,
|
2019-03-27 02:26:21 +00:00
|
|
|
merge_links,
|
2019-03-21 09:35:41 +00:00
|
|
|
urlencode,
|
2019-03-31 02:25:10 +00:00
|
|
|
htmlencode,
|
|
|
|
urldecode,
|
2017-10-23 09:58:41 +00:00
|
|
|
derived_link_info,
|
2019-03-19 22:09:46 +00:00
|
|
|
wget_output_path,
|
2019-03-27 02:26:21 +00:00
|
|
|
enforce_types,
|
2019-03-27 14:31:22 +00:00
|
|
|
TimedProgress,
|
2019-03-27 15:39:51 +00:00
|
|
|
copy_and_overwrite,
|
2019-03-27 22:24:57 +00:00
|
|
|
atomic_write,
|
2017-10-23 09:58:41 +00:00
|
|
|
)
|
2019-04-02 20:36:41 +00:00
|
|
|
from core.parse import parse_links
|
|
|
|
from core.links import validate_links
|
|
|
|
from core.logs import (
|
2019-03-22 19:09:39 +00:00
|
|
|
log_indexing_process_started,
|
2019-03-21 05:28:12 +00:00
|
|
|
log_indexing_started,
|
|
|
|
log_indexing_finished,
|
|
|
|
log_parsing_started,
|
|
|
|
log_parsing_finished,
|
|
|
|
)
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-19 22:09:46 +00:00
|
|
|
TITLE_LOADING_MSG = 'Not yet archived...'
|
|
|
|
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-27 22:24:30 +00:00
|
|
|
|
|
|
|
|
2017-10-18 22:38:17 +00:00
|
|
|
### Homepage index for all the links
|
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def write_links_index(links: List[Link], out_dir: str=OUTPUT_DIR, finished: bool=False) -> None:
|
2017-10-18 22:38:17 +00:00
|
|
|
"""create index.html file for a given list of links"""
|
|
|
|
|
2019-03-22 19:09:39 +00:00
|
|
|
log_indexing_process_started()
|
2019-02-21 22:45:28 +00:00
|
|
|
|
2019-03-22 19:09:39 +00:00
|
|
|
log_indexing_started(out_dir, 'index.json')
|
2019-03-27 14:31:22 +00:00
|
|
|
timer = TimedProgress(TIMEOUT * 2, prefix=' ')
|
2019-03-27 22:24:30 +00:00
|
|
|
write_json_links_index(links, out_dir=out_dir)
|
2019-03-27 14:31:22 +00:00
|
|
|
timer.end()
|
2019-03-21 05:28:12 +00:00
|
|
|
log_indexing_finished(out_dir, 'index.json')
|
2019-02-07 06:06:21 +00:00
|
|
|
|
2019-03-22 19:09:39 +00:00
|
|
|
log_indexing_started(out_dir, 'index.html')
|
2019-03-27 14:31:22 +00:00
|
|
|
timer = TimedProgress(TIMEOUT * 2, prefix=' ')
|
2019-03-27 22:24:30 +00:00
|
|
|
write_html_links_index(links, out_dir=out_dir, finished=finished)
|
2019-03-27 14:31:22 +00:00
|
|
|
timer.end()
|
2019-03-21 05:28:12 +00:00
|
|
|
log_indexing_finished(out_dir, 'index.html')
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
|
|
|
|
@enforce_types
|
2019-03-27 03:25:07 +00:00
|
|
|
def load_links_index(out_dir: str=OUTPUT_DIR, import_path: Optional[str]=None) -> Tuple[List[Link], List[Link]]:
|
2019-03-21 05:28:12 +00:00
|
|
|
"""parse and load existing index with any new links from import_path merged in"""
|
|
|
|
|
2019-03-26 07:20:41 +00:00
|
|
|
existing_links: List[Link] = []
|
2019-03-21 05:28:12 +00:00
|
|
|
if out_dir:
|
2019-03-26 23:21:34 +00:00
|
|
|
existing_links = list(parse_json_links_index(out_dir))
|
2019-03-21 05:28:12 +00:00
|
|
|
|
2019-03-26 07:20:41 +00:00
|
|
|
new_links: List[Link] = []
|
2019-03-21 05:28:12 +00:00
|
|
|
if import_path:
|
|
|
|
# parse and validate the import file
|
|
|
|
log_parsing_started(import_path)
|
|
|
|
raw_links, parser_name = parse_links(import_path)
|
2019-03-26 23:21:34 +00:00
|
|
|
new_links = list(validate_links(raw_links))
|
2019-03-21 05:28:12 +00:00
|
|
|
|
|
|
|
# merge existing links in out_dir and new links
|
2019-03-26 23:21:34 +00:00
|
|
|
all_links = list(validate_links(existing_links + new_links))
|
2019-03-21 05:28:12 +00:00
|
|
|
|
|
|
|
if import_path and parser_name:
|
2019-03-27 19:16:53 +00:00
|
|
|
num_parsed = len(raw_links)
|
|
|
|
num_new_links = len(all_links) - len(existing_links)
|
|
|
|
log_parsing_finished(num_parsed, num_new_links, parser_name)
|
2019-03-21 05:28:12 +00:00
|
|
|
|
|
|
|
return all_links, new_links
|
2017-07-04 10:38:07 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def write_json_links_index(links: List[Link], out_dir: str=OUTPUT_DIR) -> None:
|
2017-10-18 22:38:17 +00:00
|
|
|
"""write the json link index to a given path"""
|
|
|
|
|
2019-03-26 23:21:34 +00:00
|
|
|
assert isinstance(links, List), 'Links must be a list, not a generator.'
|
|
|
|
assert isinstance(links[0].history, dict)
|
|
|
|
assert isinstance(links[0].sources, list)
|
|
|
|
|
|
|
|
if links[0].history.get('title'):
|
|
|
|
assert isinstance(links[0].history['title'][0], ArchiveResult)
|
|
|
|
|
|
|
|
if links[0].sources:
|
|
|
|
assert isinstance(links[0].sources[0], str)
|
2019-02-21 22:45:28 +00:00
|
|
|
|
2017-10-18 22:38:17 +00:00
|
|
|
path = os.path.join(out_dir, 'index.json')
|
|
|
|
|
2019-03-27 19:32:39 +00:00
|
|
|
index_json = {
|
|
|
|
'info': 'ArchiveBox Index',
|
|
|
|
'source': 'https://github.com/pirate/ArchiveBox',
|
|
|
|
'docs': 'https://github.com/pirate/ArchiveBox/wiki',
|
|
|
|
'version': VERSION,
|
|
|
|
'num_links': len(links),
|
|
|
|
'updated': datetime.now(),
|
|
|
|
'links': links,
|
|
|
|
}
|
2019-03-27 22:24:57 +00:00
|
|
|
atomic_write(index_json, path)
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-26 23:21:34 +00:00
|
|
|
def parse_json_links_index(out_dir: str=OUTPUT_DIR) -> Iterator[Link]:
|
2019-03-21 05:28:12 +00:00
|
|
|
"""parse a archive index json file and return the list of links"""
|
2019-03-26 23:21:34 +00:00
|
|
|
|
2017-10-23 09:58:41 +00:00
|
|
|
index_path = os.path.join(out_dir, 'index.json')
|
|
|
|
if os.path.exists(index_path):
|
|
|
|
with open(index_path, 'r', encoding='utf-8') as f:
|
2019-02-21 22:45:28 +00:00
|
|
|
links = json.load(f)['links']
|
2019-03-27 14:31:07 +00:00
|
|
|
for link_json in links:
|
2019-03-30 19:03:46 +00:00
|
|
|
yield Link.from_json(link_json)
|
2017-10-23 09:58:41 +00:00
|
|
|
|
2019-03-26 23:21:34 +00:00
|
|
|
return ()
|
2017-10-23 09:58:41 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:57 +00:00
|
|
|
def write_html_links_index(links: List[Link], out_dir: str=OUTPUT_DIR, finished: bool=False) -> None:
|
2017-10-18 22:38:17 +00:00
|
|
|
"""write the html link index to a given path"""
|
|
|
|
|
2019-03-27 22:24:57 +00:00
|
|
|
copy_and_overwrite(
|
|
|
|
os.path.join(TEMPLATES_DIR, 'static'),
|
|
|
|
os.path.join(out_dir, 'static'),
|
|
|
|
)
|
2018-04-17 11:00:40 +00:00
|
|
|
|
2019-03-27 22:24:57 +00:00
|
|
|
atomic_write('User-agent: *\nDisallow: /', os.path.join(out_dir, 'robots.txt'))
|
2018-09-12 23:25:48 +00:00
|
|
|
|
2018-06-11 01:12:55 +00:00
|
|
|
with open(os.path.join(TEMPLATES_DIR, 'index.html'), 'r', encoding='utf-8') as f:
|
2017-07-04 10:38:07 +00:00
|
|
|
index_html = f.read()
|
|
|
|
|
2018-06-11 01:12:55 +00:00
|
|
|
with open(os.path.join(TEMPLATES_DIR, 'index_row.html'), 'r', encoding='utf-8') as f:
|
2017-10-18 22:38:17 +00:00
|
|
|
link_row_html = f.read()
|
2017-07-04 10:38:07 +00:00
|
|
|
|
2019-03-31 01:29:16 +00:00
|
|
|
link_rows = []
|
|
|
|
for link in links:
|
|
|
|
template_row_vars: Mapping[str, str] = {
|
2019-03-26 23:21:34 +00:00
|
|
|
**derived_link_info(link),
|
2019-03-19 22:09:46 +00:00
|
|
|
'title': (
|
2019-03-26 23:21:34 +00:00
|
|
|
link.title
|
|
|
|
or (link.base_url if link.is_archived else TITLE_LOADING_MSG)
|
2019-03-19 22:09:46 +00:00
|
|
|
),
|
2019-03-27 07:49:39 +00:00
|
|
|
'tags': (link.tags or '') + (' {}'.format(link.extension) if link.is_static else ''),
|
2019-03-20 08:30:00 +00:00
|
|
|
'favicon_url': (
|
2019-03-26 23:21:34 +00:00
|
|
|
os.path.join('archive', link.timestamp, 'favicon.ico')
|
2019-03-20 08:30:00 +00:00
|
|
|
# if link['is_archived'] else 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
|
|
|
|
),
|
2019-03-21 09:35:41 +00:00
|
|
|
'archive_url': urlencode(
|
2019-03-19 22:09:46 +00:00
|
|
|
wget_output_path(link) or 'index.html'
|
|
|
|
),
|
2019-03-31 01:29:16 +00:00
|
|
|
}
|
|
|
|
link_rows.append(Template(link_row_html).substitute(**template_row_vars))
|
2017-07-04 10:38:07 +00:00
|
|
|
|
2019-03-31 01:29:16 +00:00
|
|
|
template_vars: Mapping[str, str] = {
|
|
|
|
'num_links': str(len(links)),
|
2017-07-04 11:24:03 +00:00
|
|
|
'date_updated': datetime.now().strftime('%Y-%m-%d'),
|
|
|
|
'time_updated': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
2018-04-17 13:14:01 +00:00
|
|
|
'footer_info': FOOTER_INFO,
|
2019-03-27 19:35:13 +00:00
|
|
|
'version': VERSION,
|
2018-04-17 13:14:01 +00:00
|
|
|
'git_sha': GIT_SHA,
|
2019-03-31 01:29:16 +00:00
|
|
|
'rows': '\n'.join(link_rows),
|
2019-03-19 22:08:42 +00:00
|
|
|
'status': 'finished' if finished else 'running',
|
2017-07-04 11:24:03 +00:00
|
|
|
}
|
2019-03-31 01:29:16 +00:00
|
|
|
template_html = Template(index_html).substitute(**template_vars)
|
2017-07-04 10:38:07 +00:00
|
|
|
|
2019-03-31 01:29:16 +00:00
|
|
|
atomic_write(template_html, os.path.join(out_dir, 'index.html'))
|
2017-07-05 21:33:51 +00:00
|
|
|
|
|
|
|
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-26 07:20:41 +00:00
|
|
|
def patch_links_index(link: Link, out_dir: str=OUTPUT_DIR) -> None:
|
2019-03-19 22:09:46 +00:00
|
|
|
"""hack to in-place update one row's info in the generated index html"""
|
|
|
|
|
2019-03-26 23:21:34 +00:00
|
|
|
title = link.title or link.latest_outputs()['title']
|
|
|
|
successful = link.num_outputs
|
2019-02-21 22:45:28 +00:00
|
|
|
|
2019-03-19 22:09:46 +00:00
|
|
|
# Patch JSON index
|
2019-03-21 05:28:12 +00:00
|
|
|
json_file_links = parse_json_links_index(out_dir)
|
2019-03-26 23:21:34 +00:00
|
|
|
patched_links = []
|
2019-03-21 05:28:12 +00:00
|
|
|
for saved_link in json_file_links:
|
2019-03-26 23:21:34 +00:00
|
|
|
if saved_link.url == link.url:
|
2019-03-27 07:49:39 +00:00
|
|
|
patched_links.append(saved_link.overwrite(
|
|
|
|
title=title,
|
|
|
|
history=link.history,
|
|
|
|
updated=link.updated,
|
|
|
|
))
|
2019-03-26 23:21:34 +00:00
|
|
|
else:
|
|
|
|
patched_links.append(saved_link)
|
|
|
|
|
2019-03-27 22:24:30 +00:00
|
|
|
write_json_links_index(patched_links, out_dir=out_dir)
|
2019-02-21 22:45:28 +00:00
|
|
|
|
2019-03-19 22:09:46 +00:00
|
|
|
# Patch HTML index
|
2019-03-21 05:28:12 +00:00
|
|
|
html_path = os.path.join(out_dir, 'index.html')
|
2019-03-19 22:09:46 +00:00
|
|
|
html = open(html_path, 'r').read().split('\n')
|
|
|
|
for idx, line in enumerate(html):
|
2019-03-26 23:21:34 +00:00
|
|
|
if title and ('<span data-title-for="{}"'.format(link.url) in line):
|
2019-03-19 22:09:46 +00:00
|
|
|
html[idx] = '<span>{}</span>'.format(title)
|
2019-03-26 23:21:34 +00:00
|
|
|
elif successful and ('<span data-number-for="{}"'.format(link.url) in line):
|
2019-03-19 22:09:46 +00:00
|
|
|
html[idx] = '<span>{}</span>'.format(successful)
|
|
|
|
break
|
|
|
|
|
2019-03-27 22:24:57 +00:00
|
|
|
atomic_write('\n'.join(html), html_path)
|
2019-02-21 22:45:28 +00:00
|
|
|
|
2019-03-21 05:28:12 +00:00
|
|
|
|
2017-10-23 09:58:41 +00:00
|
|
|
### Individual link index
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def write_link_index(link: Link, link_dir: Optional[str]=None) -> None:
|
|
|
|
link_dir = link_dir or link.link_dir
|
|
|
|
|
|
|
|
write_json_link_index(link, link_dir)
|
|
|
|
write_html_link_index(link, link_dir)
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def write_json_link_index(link: Link, link_dir: Optional[str]=None) -> None:
|
2017-10-18 22:38:17 +00:00
|
|
|
"""write a json file with some info about the link"""
|
|
|
|
|
2019-03-27 22:24:30 +00:00
|
|
|
link_dir = link_dir or link.link_dir
|
|
|
|
path = os.path.join(link_dir, 'index.json')
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-27 22:24:57 +00:00
|
|
|
atomic_write(link._asdict(), path)
|
2017-10-18 22:38:17 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def parse_json_link_index(link_dir: str) -> Optional[Link]:
|
2017-10-23 09:58:41 +00:00
|
|
|
"""load the json link index from a given directory"""
|
2019-03-27 22:24:30 +00:00
|
|
|
existing_index = os.path.join(link_dir, 'index.json')
|
2017-10-23 09:58:41 +00:00
|
|
|
if os.path.exists(existing_index):
|
|
|
|
with open(existing_index, 'r', encoding='utf-8') as f:
|
2019-02-21 22:45:28 +00:00
|
|
|
link_json = json.load(f)
|
2019-03-30 19:38:28 +00:00
|
|
|
return Link.from_json(link_json)
|
2019-03-26 23:21:34 +00:00
|
|
|
return None
|
2017-10-23 09:58:41 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def load_json_link_index(link: Link, link_dir: Optional[str]=None) -> Link:
|
2019-03-21 05:28:12 +00:00
|
|
|
"""check for an existing link archive in the given directory,
|
|
|
|
and load+merge it into the given link dict
|
|
|
|
"""
|
2019-03-27 22:24:30 +00:00
|
|
|
link_dir = link_dir or link.link_dir
|
|
|
|
existing_link = parse_json_link_index(link_dir)
|
2019-03-27 02:26:21 +00:00
|
|
|
if existing_link:
|
|
|
|
return merge_links(existing_link, link)
|
|
|
|
return link
|
2019-03-21 05:28:12 +00:00
|
|
|
|
2019-03-26 09:33:34 +00:00
|
|
|
|
2019-03-27 02:26:21 +00:00
|
|
|
@enforce_types
|
2019-03-27 22:24:30 +00:00
|
|
|
def write_html_link_index(link: Link, link_dir: Optional[str]=None) -> None:
|
|
|
|
link_dir = link_dir or link.link_dir
|
|
|
|
|
2019-03-08 22:46:14 +00:00
|
|
|
with open(os.path.join(TEMPLATES_DIR, 'link_index.html'), 'r', encoding='utf-8') as f:
|
2017-10-18 22:38:17 +00:00
|
|
|
link_html = f.read()
|
|
|
|
|
2019-03-27 22:24:57 +00:00
|
|
|
path = os.path.join(link_dir, 'index.html')
|
|
|
|
|
2019-03-31 02:25:10 +00:00
|
|
|
template_vars: Mapping[str, str] = {
|
2019-03-27 22:24:57 +00:00
|
|
|
**derived_link_info(link),
|
|
|
|
'title': (
|
|
|
|
link.title
|
|
|
|
or (link.base_url if link.is_archived else TITLE_LOADING_MSG)
|
|
|
|
),
|
2019-03-31 02:25:10 +00:00
|
|
|
'url_str': htmlencode(urldecode(link.base_url)),
|
2019-03-27 22:24:57 +00:00
|
|
|
'archive_url': urlencode(
|
|
|
|
wget_output_path(link)
|
|
|
|
or (link.domain if link.is_archived else 'about:blank')
|
|
|
|
),
|
|
|
|
'extension': link.extension or 'html',
|
|
|
|
'tags': link.tags or 'untagged',
|
|
|
|
'status': 'archived' if link.is_archived else 'not yet archived',
|
|
|
|
'status_color': 'success' if link.is_archived else 'danger',
|
2019-04-02 20:36:41 +00:00
|
|
|
'oldest_archive_date': ts_to_date(link.oldest_archive_date),
|
2019-03-31 02:25:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
html_index = Template(link_html).substitute(**template_vars)
|
2019-03-27 22:24:57 +00:00
|
|
|
|
|
|
|
atomic_write(html_index, path)
|