Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9337563
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
20 KB
Subscribers
None
View Options
diff --git a/PKG-INFO b/PKG-INFO
index 32dc5e7d..d89a2337 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,10 +1,10 @@
Metadata-Version: 1.0
Name: swh.web.ui
-Version: 0.0.7
+Version: 0.0.8
Summary: Software Heritage Web UI
Home-page: https://forge.softwareheritage.org/diffusion/DWUI/
Author: Software Heritage developers
Author-email: swh-devel@inria.fr
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN
diff --git a/README b/README
index af0fa908..11e51015 100644
--- a/README
+++ b/README
@@ -1,24 +1,24 @@
swh-web-ui
==========
SWH's web application
# Configuration file sample
-~/.config/swh/web-ui.ini
+~/.config/swh/webapp.ini
[main]
# where to log information
log_dir = /tmp/swh/web-ui/log
# for dev only
debug = true
# current server (0.0.0.0 for world opening)
host = 127.0.0.1
# its port
port = 6543
# the backend this server communicates to
api_backend = http://127.0.0.1:5000
diff --git a/swh.web.ui.egg-info/PKG-INFO b/swh.web.ui.egg-info/PKG-INFO
index 32dc5e7d..d89a2337 100644
--- a/swh.web.ui.egg-info/PKG-INFO
+++ b/swh.web.ui.egg-info/PKG-INFO
@@ -1,10 +1,10 @@
Metadata-Version: 1.0
Name: swh.web.ui
-Version: 0.0.7
+Version: 0.0.8
Summary: Software Heritage Web UI
Home-page: https://forge.softwareheritage.org/diffusion/DWUI/
Author: Software Heritage developers
Author-email: swh-devel@inria.fr
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN
diff --git a/swh/web/ui/controller.py b/swh/web/ui/controller.py
old mode 100755
new mode 100644
index 539fc8e1..8ffa879f
--- a/swh/web/ui/controller.py
+++ b/swh/web/ui/controller.py
@@ -1,328 +1,333 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import logging
-from flask import redirect, render_template, url_for, flash, jsonify, request
+from flask import redirect, render_template, url_for, jsonify, request
from flask import make_response
from swh.core.hashutil import ALGORITHMS
from swh.web.ui.main import app
from swh.web.ui import service, query
from swh.web.ui.decorators import jsonp
hash_filter_keys = ALGORITHMS
@app.route('/')
def main():
"""Main application view.
At the moment, redirect to the content search view.
"""
return redirect(url_for('info'))
@app.route('/info')
def info():
"""A simple api to define what the server is all about.
"""
logging.info('Dev SWH UI')
return 'Dev SWH UI'
@app.route('/search')
def search():
"""Search for hashes in swh-storage.
"""
q = request.args.get('q', '')
+ env = {'q': q, 'message': '', 'found': None}
- if q:
- flash("Search hash '%s' posted!" % q)
- message = service.lookup_hash(q)
- else:
- message = ''
+ try:
+ if q:
+ env['found'] = service.lookup_hash(q)
+ except ValueError:
+ env['message'] = 'Error: invalid query string'
- return render_template('search.html',
- q=q,
- message=message)
+ return render_template('search.html', **env)
@app.route('/browse/revision/<sha1_git>')
def revision(sha1_git):
"""Show commit information.
Args:
sha1_git: the revision's sha1
Returns:
Revision information
"""
return render_template('revision.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>')
def directory(sha1_git):
"""Show directory information.
Args:
sha1_git: the directory's sha1
Returns:
Directory information
"""
return render_template('directory.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>/<path:p>')
def directory_at_path(sha1_git, p):
"""Show directory information for the sha1_git at path.
Args:
sha1_git: the directory's sha1
path: file or directory pointed to
Returns:
Directory information at sha1_git + path
"""
return render_template('directory.html',
sha1_git=sha1_git,
path=p)
@app.route('/browse/content/<hash>:<sha>')
def content(hash, sha):
"""Show content information.
Args:
hash: hash according to HASH_ALGO, where HASH_ALGO is
one of: sha1, sha1_git, sha256. This means that several different URLs (at
least one per HASH_ALGO) will point to the same content
sha: the sha with 'hash' format
Returns:
The content's information at sha1_git
"""
# Checks user input
if hash not in hash_filter_keys:
return make_response(
'Bad request, sha must be one of sha1, sha1_git, sha256',
400)
h = query.categorize_hash(sha)
if h == {}:
return make_response(
'Bad request, %s is not of type %s' % (sha, hash),
400)
if hash == 'sha256' and not h.get(hash):
return make_response(
'Bad request, %s is not of type sha256' % (sha,),
400)
if hash != 'sha256' and not h.get('sha1') and not h.get('sha1_git'):
return make_response(
'Bad request, %s is not of type sha1 or sha1_git' % (sha,),
400)
message = service.lookup_hash_origin(h)
return render_template('content.html',
hash=hash,
sha=sha,
message=message)
@app.route('/browse/release/<sha1_git>')
def release(sha1_git):
"""Show release's information.
Args:
sha1_git: sha1_git for this particular release
Returns:
Release's information
"""
return 'Release information at %s' % sha1_git
@app.route('/browse/person/<int:id>')
def person(id):
"""Show Person's information at id.
Args:
id: person's unique identifier
Returns:
Person's information
"""
return 'Person information at %s' % id
@app.route('/browse/origin/<int:id>')
def origin(id):
"""Show origin's information at id.
Args:
id: origin's unique identifier
Returns:
Origin's information
"""
return 'Origin information at %s' % id
@app.route('/browse/project/<int:id>')
def project(id):
"""Show project's information at id.
Args:
id: project's unique identifier
Returns:
Project's information
"""
return 'Project information at %s' % id
@app.route('/browse/organization/<int:id>')
def organization(id):
"""Show organization's information at id.
Args:
id: organization's unique identifier
Returns:
Organization's information
"""
return 'Organization information at %s' % id
@app.route('/browse/directory/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/'
'<path:branch>|/<path:path>')
def directory_at_origin(timestamp, origin_type, origin_url, branch, path):
"""Show directory information at timestamp, origin-type, origin-url, branch
and path.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain `/`
path: path to directory or file
Returns:
Directory information at the given parameters.
"""
return 'Directory at (%s, %s, %s, %s, %s)' % (timestamp,
origin_type,
origin_url,
branch,
path)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/<path:branch>')
def revision_at_origin_and_branch(timestamp, origin_type, origin_url, branch):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain /
Returns:
Revision information at the given parameters.
"""
return 'Revision at (ts=%s, type=%s, url=%s, branch=%s)' % (timestamp,
origin_type,
origin_url,
branch)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|')
def revision_at_origin(timestamp, origin_type, origin_url):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or iso8601
date
format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
Returns:
Revision information at the given parameters.
"""
return 'Revision at (timestamp=%s, type=%s, url=%s)' % (timestamp,
origin_type,
origin_url)
@app.route('/api/1/stat/counters')
@jsonp
def api_stats():
"""Return statistics as a JSON object"""
return jsonify(service.stat_counters())
+@app.route('/api/1/search/<string:q>/')
+@jsonp
+def api_search(q):
+ """Return search results as a JSON object"""
+ return jsonify({'query': q,
+ 'found': service.lookup_hash(q)})
+
+
def run(conf):
"""Run the api's server.
Args:
conf is a dictionary of keywords:
- 'db_url' the db url's access (through psycopg2 format)
- 'content_storage_dir' revisions/directories/contents storage on disk
- 'host' to override the default 127.0.0.1 to open or not the server
to the world
- 'port' to override the default of 5000 (from the underlying layer:
flask)
- 'debug' activate the verbose logs
- 'secret_key' the flask secret key
Returns:
Never
Raises:
?
"""
- print("""SWH Web UI run
-host: %s
-port: %s
+ print("""SWH Web UI available at http://%s:%s/
debug: %s""" % (conf['host'], conf.get('port', None), conf['debug']))
app.secret_key = conf['secret_key']
app.config.update({'conf': conf})
app.run(host=conf['host'],
port=conf.get('port', None),
debug=conf['debug'])
diff --git a/swh/web/ui/query.py b/swh/web/ui/query.py
index 96579c68..06d1306d 100644
--- a/swh/web/ui/query.py
+++ b/swh/web/ui/query.py
@@ -1,40 +1,58 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import re
-from swh.core import hashutil
+from swh.core.hashutil import ALGORITHMS, hex_to_hash
-# Regexp to filter and check inputs
-sha256_regexp = '[0-9a-f]{64}'
-sha1_regexp = '[0-9a-f]{40}'
+SHA256_RE = re.compile(r'^[0-9a-f]{64}$', re.IGNORECASE)
+SHA1_RE = re.compile(r'^[0-9a-f]{40}$', re.IGNORECASE)
-def categorize_hash(hash):
- """Categorize the hash string according to what it is.
+def parse_hash(q):
+ """Detect the hash type of a user submitted query string.
Args:
- hash: hash string representation (sha1 or sha256)
+ query string with the following format: "[HASH_TYPE:]HEX_CHECKSUM",
+ where HASH_TYPE is optional, defaults to "sha1", and can be one of
+ swh.core.hashutil.ALGORITHMS
Returns:
- A dictionary of hash indexed by their nature (sha1, sha256)
- The dictionary will be empty if nothing matches
+ A pair (hash_algorithm, byte hash value)
Raises:
- None
+ ValueError if the given query string does not correspond to a valid
+ hash value
"""
- try:
- h = hashutil.hex_to_hash(hash)
- except ValueError: # ignore silently to check the other inputs
- return {}
-
- if re.search(sha256_regexp, hash):
- return {'sha256': h}
- if re.search(sha1_regexp, hash):
- return {'sha1': h}
- return {}
+ def guess_algo(q):
+ if SHA1_RE.match(q):
+ return 'sha1'
+ elif SHA256_RE.match(q):
+ return 'sha256'
+ else:
+ raise ValueError('invalid checksum query string')
+
+ def check_algo(algo, hex):
+ if (algo in set(['sha1', 'sha1_git']) and not SHA1_RE.match(hex)) \
+ or (algo == 'sha256' and not SHA256_RE.match(hex)):
+ raise ValueError('invalid hash for algorithm ' + algo)
+
+ parts = q.split(':')
+ if len(parts) > 2:
+ raise ValueError('invalid checksum query string')
+ elif len(parts) == 1:
+ parts = (guess_algo(q), q)
+ elif len(parts) == 2:
+ check_algo(parts[0], parts[1])
+ algo = parts[0]
+ hash = hex_to_hash(parts[1])
+
+ if algo not in ALGORITHMS:
+ raise ValueError('unknown hash algorithm: ' + algo)
+
+ return (algo, hash)
diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
old mode 100755
new mode 100644
index e2fb8533..9ae49c75
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -1,84 +1,77 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.web.ui import main
from swh.web.ui import query
def lookup_hash(q):
- """Given a string query q of one hash, lookup its hash to the backend.
+ """Checks if the storage contains a given content checksum
- Args:
- query, hash as a string (sha1, sha256, etc...)
+ Args: query string
Returns:
- a string message (found, not found or a potential error explanation)
+ True or False, according to whether the checksum is present or not
- Raises:
- OSError (no route to host), etc... Network issues in general
"""
- hash = query.categorize_hash(q)
- if hash != {}:
- present = main.storage().content_exist(hash)
- return 'Found!' if present else 'Not Found'
- return """This is not a hash.
-Hint: hexadecimal string with length either 20 (sha1) or 32 (sha256)."""
+ (algo, hash) = query.parse_hash(q)
+ return main.storage().content_exist({algo: hash})
def _origin_seen(hash, data):
"""Given an origin, compute a message string with the right information.
Args:
origin: a dictionary with keys:
- origin: a dictionary with type and url keys
- occurrence: a dictionary with a validity range
Returns:
message as a string
"""
if data is None:
return 'Content with hash %s is unknown as of now.' % hash
origin_type = data['origin_type']
origin_url = data['origin_url']
revision = data['revision']
branch = data['branch']
path = data['path']
- print("data:", data)
+
return """The content with hash %s has been seen on origin with type '%s'
at url '%s'. The revision was identified at '%s' on branch '%s'.
The file's path referenced was '%s'.""" % (hash,
origin_type,
origin_url,
revision,
branch,
path)
def lookup_hash_origin(hash):
"""Given a hash, return the origin of such content if any is found.
Args:
hash: key/value dictionary
Returns:
The origin for such hash if it's found.
Raises:
OSError (no route to host), etc... Network issues in general
"""
data = main.storage().content_find_occurrence(hash)
return _origin_seen(hash, data)
def stat_counters():
"""Return the stat counters for Software Heritage
Returns:
A dict mapping textual labels to integer values.
"""
return main.storage().stat_counters()
diff --git a/swh/web/ui/templates/layout.html b/swh/web/ui/templates/layout.html
index aab291e6..279c731a 100644
--- a/swh/web/ui/templates/layout.html
+++ b/swh/web/ui/templates/layout.html
@@ -1,10 +1,11 @@
<!doctype html>
-<title>SWH Web UI</title>
-<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
-<div class=page>
- <div class=metanav>
+<title>Software Heritage Archive</title>
+<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
+<div class="page">
+ <div class="metanav">
{% for message in get_flashed_messages() %}
- <div class=flash>{{ message }}</div>
+ <div class="flash">{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
+ </div>
</div>
diff --git a/swh/web/ui/templates/search.html b/swh/web/ui/templates/search.html
index 26f9a1fb..77c0a14d 100644
--- a/swh/web/ui/templates/search.html
+++ b/swh/web/ui/templates/search.html
@@ -1,13 +1,16 @@
{% extends "layout.html" %}
{% block body %}
-<form action="{{ url_for('search') }}" method=get class=search>
- <dl>
- <dt>Hashes (colon separated values):
- <dd><input type=text size=32 name=q value={{ q }}>
- <dd><input type=submit value=Search>
- </dl>
+<div id="search_form">
+ <form action="{{ url_for('search') }}" method="get" class="search">
+ Content hash:
+ <input type="text" size="70" name="q" value="{{ q }}">
+ <input type="submit" value="Search">
</form>
- <ul class=entries>
- <li><h2>{{ message | safe }}</h2>
- </ul>
+</div>
+{% if message is not none %}
+<div id="message">{{ message | safe }}</div>
+{% endif %}
+{% if found is not none %}
+<div id="search_results">Found: {{ found | safe }}</div>
+{% endif %}
{% endblock %}
diff --git a/swh/web/ui/tests/test_query.py b/swh/web/ui/tests/test_query.py
index f387a6e9..dbae2df7 100644
--- a/swh/web/ui/tests/test_query.py
+++ b/swh/web/ui/tests/test_query.py
@@ -1,36 +1,33 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import unittest
from nose.tools import istest
from swh.web.ui import query
from swh.core import hashutil
class QueryTestCase(unittest.TestCase):
- @istest
- def categorize_hash(self):
- input_sha1 = 'f1d2d2f924e986ac86fdf7b36c94bcdf32beec15'
-
- res = query.categorize_hash(input_sha1)
-
- self.assertEquals(res, {'sha1': hashutil.hex_to_hash(input_sha1)})
-
- def categorize_hash_2(self):
- input_sha256 = \
- '084c799cd551dd1d8d5c5f9a5d593b2e931f5e36122ee5c793c1d08a19839cc0'
-
- res = query.categorize_hash(input_sha256)
- self.assertEquals(res, {'sha256': hashutil.hex_to_hash(input_sha256)})
-
- def categorize_hash_3(self):
- input_bad_length = '1234567890987654'
+ @istest
+ def parse_hash(self):
+ q = 'f1d2d2f924e986ac86fdf7b36c94bcdf32beec15'
+ r = query.parse_hash(q)
+ self.assertEquals(r, ('sha1', hashutil.hex_to_hash(q)))
- res = query.categorize_hash(input_bad_length)
+ @istest
+ def parse_hash_2(self):
+ q = '084C799CD551DD1D8D5C5F9A5D593B2' \
+ 'E931F5E36122ee5c793c1d08a19839cc0'
+ r = query.parse_hash(q)
+ self.assertEquals(r, ('sha256', hashutil.hex_to_hash(q)))
- self.assertEquals(res, {})
+ @istest
+ def parse_hash_3(self):
+ q = '1234567890987654'
+ with self.assertRaises(ValueError):
+ query.parse_hash(q)
diff --git a/version.txt b/version.txt
index 5b14ec5e..33f71f9b 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v0.0.7-0-gad9eafc
\ No newline at end of file
+v0.0.8-0-g69d124a
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jul 4 2025, 8:10 AM (10 w, 15 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3215021
Attached To
rDWAPPS Web applications
Event Timeline
Log In to Comment