Skip to content

Commit 194cb4c

Browse files
committed
Add route, view and helpers for exporting d/copyright to spdx
1 parent eda9ff9 commit 194cb4c

File tree

5 files changed

+203
-3
lines changed

5 files changed

+203
-3
lines changed

debsources/app/copyright/routes.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
from __future__ import absolute_import
1313

1414

15-
from flask import jsonify
15+
from flask import jsonify, make_response
1616

1717
from ..helper import bind_render
1818
from . import bp_copyright
1919
from ..views import (IndexView, PrefixView, ListPackagesView, ErrorHandler,
2020
Ping, PackageVersionsView, DocView, AboutView, SearchView)
21-
from .views import LicenseView, ChecksumLicenseView, SearchFileView, StatsView
21+
from .views import (LicenseView, ChecksumLicenseView, SearchFileView,
22+
StatsView, SPDXView)
2223

2324

2425
# context vars
@@ -254,3 +255,11 @@ def skeleton_variables():
254255
render_func=jsonify,
255256
err_func=ErrorHandler(mode='json'),
256257
get_objects='stats_suite'))
258+
259+
# SDPX view
260+
bp_copyright.add_url_rule(
261+
'/spdx/<path:path_to>/',
262+
view_func=SPDXView.as_view(
263+
'spdx',
264+
render_func=make_response,
265+
err_func=ErrorHandler(mode='json')))

debsources/app/copyright/templates/copyright/license.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ <h2>{{ self.title() }} / {{ version }}</h2>
2727
{% if dump == 'True' %}
2828
{% include "source_file_code.inc.html" %}
2929
{% else %}
30+
<div class="warning"><a href="{{url_for('.spdx', path_to=package + '/' + version) }}">Export to SPDX</a></div>
3031
{% include "copyright/license_render.inc.html" %}
3132
{% endif %}
3233
{% endblock %}

debsources/app/copyright/views.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,53 @@ def get_stats(self):
339339
dual_results=dual_res,
340340
dual_licenses=sorted(dual_licenses),
341341
suites=all_suites)
342+
343+
344+
class SPDXView(GeneralView):
345+
346+
def _generate_file(self, spdx_values):
347+
output = ''
348+
for value in spdx_values:
349+
output += value.decode('utf-8') + '\n'
350+
return output
351+
352+
def get_objects(self, path_to):
353+
path_dict = path_to.split('/')
354+
355+
package = path_dict[0]
356+
version = path_dict[1]
357+
path = '/'.join(path_dict[2:])
358+
359+
if version == "latest": # we search the latest available version
360+
return self._handle_latest_version(request.endpoint,
361+
package, path)
362+
363+
versions = self.handle_versions(version, package, path)
364+
if versions:
365+
redirect_url_parts = [package, versions[-1]]
366+
if path:
367+
redirect_url_parts.append(path)
368+
redirect_url = '/'.join(redirect_url_parts)
369+
return self._redirect_to_url(request.endpoint,
370+
redirect_url, redirect_code=302)
371+
372+
try:
373+
sources_path = helper.get_sources_path(session, package, version,
374+
current_app.config)
375+
except FileOrFolderNotFound:
376+
raise Http404ErrorSuggestions(package, version,
377+
'debian/copyright')
378+
except InvalidPackageOrVersionError:
379+
raise Http404ErrorSuggestions(package, version, '')
380+
381+
try:
382+
c = helper.parse_license(sources_path)
383+
except Exception:
384+
# non machine readable license
385+
return dict(return_code=404)
386+
spdx = helper.export_copyright_to_spdx(
387+
c, session=session, package=package, version=version)
388+
attachment = "attachment;" + "filename=" + \
389+
path_to.replace('/', '_') + ".spdx"
390+
return dict(spdx=self._generate_file(spdx),
391+
header=attachment)

debsources/app/views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from debian.debian_support import version_compare
2020

2121
from flask import (
22-
current_app, jsonify, render_template, request, url_for, redirect)
22+
current_app, jsonify, render_template, request, url_for, redirect,
23+
make_response)
2324
from flask.views import View
2425

2526
from debsources.excepts import (
@@ -192,6 +193,10 @@ def dispatch_request(self, **kwargs):
192193
"""
193194
try:
194195
context = self.get_objects(**kwargs)
196+
if self.render_func is make_response:
197+
response = make_response(context['spdx'])
198+
response.headers["Content-Disposition"] = context['header']
199+
return response
195200
return self.render_func(**context)
196201
except Http403Error as e:
197202
return self.err_func(e, http=403)

debsources/license_helper.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
import io
1313
import logging
1414
import re
15+
import hashlib
16+
from datetime import datetime
1517

1618
from flask import url_for
1719
from debian import copyright
1820

21+
from debsources.models import Checksum, File, Package, PackageName
1922
from debsources.navigation import Location, SourceFile
2023

2124
# import debsources.query as qry
@@ -134,6 +137,10 @@ def get_license(session, package, version, path, license_path=None):
134137
return None
135138

136139

140+
def get_paragraph(c, path):
141+
return c.find_files_paragraph(path)
142+
143+
137144
def get_copyright_header(copyright):
138145
""" Return all the header attributs
139146
@@ -241,3 +248,131 @@ def anchor_to_license(copyright, synopsis):
241248
return '#license-' + str(licenses.index(synopsis))
242249
else:
243250
return None
251+
252+
253+
def export_copyright_to_spdx(c, package, version, session):
254+
""" Creates the SPDX document and saves the result in fname
255+
256+
"""
257+
258+
def create_package_code(session, package, version):
259+
sha = (session.query(Checksum.sha256.label("sha256"))
260+
.filter(Checksum.package_id == Package.id)
261+
.filter(Checksum.file_id == File.id)
262+
.filter(Package.name_id == PackageName.id)
263+
.filter(PackageName.name == package)
264+
.filter(Package.version == version)
265+
.order_by("sha256")
266+
).all()
267+
sha_values = [sha256[0] for sha256 in sha]
268+
return hashlib.sha256("".join(sha_values)).hexdigest()
269+
270+
# find out which are not standard and save SPDX required information
271+
# Non standard licenses are referenced as LicenseRed-<number>
272+
license_refs = dict()
273+
count = 0
274+
unknown_licenses = dict()
275+
for par in c.all_files_paragraphs():
276+
try:
277+
l = par.license.synopsis
278+
if l not in license_refs.keys():
279+
if not match_license(l):
280+
l_id = 'LicenseRef-' + str(count)
281+
license_refs[l] = l_id
282+
count += 1
283+
unknown_licenses[l] = "LicenseId: " + l_id + \
284+
"\nLicenseName: " + l
285+
else:
286+
license_refs[l] = 'LicenseRef-' + l
287+
except (AttributeError, ValueError):
288+
pass
289+
290+
for par in c.all_license_paragraphs():
291+
try:
292+
l = par.license.synopsis
293+
if l in license_refs.keys() and not match_license(l):
294+
unknown_licenses[l] = "LicenseID: " + license_refs[l] + \
295+
"\nExtractedText: <text>" + \
296+
par.license.text + "</text>" + \
297+
"\nLicenseName: " + l
298+
except (AttributeError, ValueError):
299+
pass
300+
301+
time = datetime.now()
302+
now = str(time.date()) + 'T' + str(time.time()).split('.')[0] + 'Z'
303+
304+
spdx = ["SPDXVersion: SPDX-2.0", "DataLicense:CC0-1.0",
305+
"SPDXID: SPDXRef-DOCUMENT",
306+
"Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package",
307+
"DocumentName: " + c.header.upstream_name,
308+
"DocumentNamespace: http://spdx.org/spdxdocs/" +
309+
"spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
310+
"LicenseListVersion: 2.0",
311+
"Creator: Person: Debsources",
312+
"Creator: Organization: Debsources",
313+
"Creator: Tool: Debsources",
314+
"Created: " + now,
315+
"CreatorComment: <text> This document was created by" +
316+
"Debsources by parsing the respective debian/copyright" +
317+
"file of the package provided by the Debian project. You" +
318+
"may follow these links: http://debian.org/ " +
319+
"http://sources.debian.net/ to get more information about " +
320+
"Debian and Debsources. </text>",
321+
"DocumentComment: <text>This document was created using" +
322+
"SPDX 2.0, version 2.3 of the SPDX License List.</text>",
323+
"PackageName: " + c.header.upstream_name,
324+
"SPDXID: SPDXRef-Package",
325+
"PackageDownloadLocation: NOASSERTION",
326+
"PackageVerificationCode: " + create_package_code(session,
327+
package,
328+
version),
329+
"PackageLicenseConcluded: NOASSERTION"]
330+
for value in set(license_refs.values()):
331+
spdx.append("PackageLicenseInfoFromFiles: " + value)
332+
333+
spdx.extend(["PackageLicenseDeclared: NOASSERTION",
334+
"PackageCopyrightText: NOASSERTION"])
335+
for files in get_files_spdx(license_refs, package, version, session, c):
336+
for item in files:
337+
spdx.append(str(item))
338+
for unknown in unknown_licenses:
339+
spdx.append(unknown_licenses[unknown])
340+
return spdx
341+
342+
343+
def get_files_spdx(license_refs, package, version, session, c):
344+
""" Get all files from the DB for a specific package and version and
345+
then create a dictionnary for the SPDX entries
346+
347+
"""
348+
files = (session.query(Checksum.sha256.label("sha256"),
349+
File.path.label("path"))
350+
.filter(Checksum.package_id == Package.id)
351+
.filter(Checksum.file_id == File.id)
352+
.filter(Package.name_id == PackageName.id)
353+
.filter(PackageName.name == package)
354+
.filter(Package.version == version)
355+
)
356+
357+
files_info = []
358+
359+
for i, f in enumerate(files.all()):
360+
par = get_paragraph(c, f.path)
361+
try:
362+
if not match_license(par.license.synopsis):
363+
license_concluded = license_refs[par.license.synopsis]
364+
else:
365+
license_concluded = par.license.synopsis
366+
except (AttributeError, ValueError):
367+
license_concluded = "None"
368+
# NOASSERTION means that the SPDX generator did not calculate that
369+
# value.
370+
sha = 'NOASSERTION' if not f.sha256 else f.sha256
371+
files_info.append(["FileName: " + f.path,
372+
"SPDXID: SPDX-FILE-REF-" + str(i),
373+
"FileChecksum: SHA256: " + sha,
374+
"LicenseConcluded: " + license_concluded,
375+
"LicenseInfoInFile: NOASSERTION",
376+
"FileCopyrightText: <text>" +
377+
par.copyright.encode('utf-8') + "</text>"])
378+
return files_info

0 commit comments

Comments
 (0)