Skip to content

Commit abf924b

Browse files
authored
Merge pull request #354 from nexB/cache-and-stream-scan-results
Cache and stream scan results
2 parents ee5958e + 8a106d3 commit abf924b

24 files changed

Lines changed: 662 additions & 61 deletions

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def read(*names, **kwargs):
7878
# caching
7979
'zc.lockfile >= 1.0.0, < 2.0.0',
8080
'yg.lockfile >= 2.0.0, < 3.0.0',
81-
'diskcache >= 1.7.0, < 1.8.0',
81+
'diskcache >= 2.0.0, < 3.0.0',
8282

8383
# textcode
8484
'Beautifulsoup >= 3.2.0, <4.0.0',
@@ -99,6 +99,7 @@ def read(*names, **kwargs):
9999
'jinja2 >= 2.7.0, < 3.0.0',
100100
'MarkupSafe >= 0.23',
101101
'colorama',
102+
'simplejson',
102103

103104
# packagedcode
104105
'requests >= 2.7.0, < 3.0.0',

src/scancode/__init__.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2015 nexB Inc. and others. All rights reserved.
2+
# Copyright (c) 2016 nexB Inc. and others. All rights reserved.
33
# http://nexb.com and https://github.com/nexB/scancode-toolkit/
44
# The ScanCode software is licensed under the Apache License version 2.0.
55
# Data generated with ScanCode require an acknowledgment.
@@ -23,3 +23,21 @@
2323
# Visit https://github.com/nexB/scancode-toolkit/ for support and download.
2424

2525
__version__ = '2.0.0rc2'
26+
27+
from os.path import dirname
28+
from os.path import abspath
29+
from os.path import getsize
30+
from os.path import getmtime
31+
from os.path import join
32+
from os.path import exists
33+
34+
from commoncode import fileutils
35+
36+
scan_src_dir = abspath(dirname(__file__))
37+
src_dir = dirname(scan_src_dir)
38+
root_dir = dirname(src_dir)
39+
cache_dir = join(root_dir, '.cache')
40+
scans_cache_dir = join(cache_dir, 'scan_results_caches')
41+
42+
if not exists(scans_cache_dir):
43+
fileutils.create_dir(scans_cache_dir)

src/scancode/api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ def get_licenses(location, min_score=0):
142142
result['matched_rule']['licenses'] = match.rule.licenses
143143
# TODO: add debug details such as matcher
144144
# result['matched_rule']['matcher'] = match.matcher
145-
145+
146146
yield result
147147

148148

149-
def get_file_infos(location):
149+
def get_file_infos(location, as_list=True):
150150
"""
151151
Return a list of dictionaries of informations collected from the file or
152152
directory at location.
@@ -177,7 +177,10 @@ def get_file_infos(location):
177177
infos['is_media'] = is_file and T.is_media or None
178178
infos['is_source'] = is_file and T.is_source or None
179179
infos['is_script'] = is_file and T.is_script or None
180-
return [infos]
180+
if as_list:
181+
return [infos]
182+
else:
183+
return infos
181184

182185

183186
def get_package_infos(location):

src/scancode/cache.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#
2+
# Copyright (c) 2016 nexB Inc. and others. All rights reserved.
3+
# http://nexb.com and https://github.com/nexB/scancode-toolkit/
4+
# The ScanCode software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode require an acknowledgment.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# When you publish or redistribute any data created with ScanCode or any ScanCode
16+
# derivative work, you must accompany this data with the following acknowledgment:
17+
#
18+
# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
19+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
20+
# ScanCode should be considered or used as legal advice. Consult an Attorney
21+
# for any legal advice.
22+
# ScanCode is a free software code scanning tool from nexB Inc. and others.
23+
# Visit https://github.com/nexB/scancode-toolkit/ for support and download.
24+
25+
from __future__ import absolute_import, print_function
26+
27+
from collections import OrderedDict
28+
import os
29+
import sys
30+
31+
from commoncode import fileutils
32+
from commoncode import timeutils
33+
34+
from scancode import scans_cache_dir
35+
36+
"""
37+
Caching scans on disk: A cache of all the scan results.
38+
39+
Each scan results for a file or directory is cached on disk.
40+
41+
The approach is to use to cache:
42+
- the results of a scan, excluding file infos keyed by the hash of a scanned file
43+
- the file infos, keyed by the path of a scanned file
44+
45+
Once a scan is completed, we iterate the caches to output the scan results using this
46+
procedure: iterate the cached file infos and for each lookup the scan details in the
47+
cached scan results. This iteration is driving the final streaming of results to the
48+
output format (e.g. JSON).
49+
50+
Finally once a scan is completed the cache is destroyed to free up disk space.
51+
"""
52+
53+
# Tracing flags
54+
TRACE = False
55+
56+
def logger_debug(*args):
57+
pass
58+
59+
if TRACE:
60+
import logging
61+
62+
logger = logging.getLogger(__name__)
63+
# logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
64+
logging.basicConfig(stream=sys.stdout)
65+
logger.setLevel(logging.DEBUG)
66+
67+
def logger_debug(*args):
68+
return logger.debug(' '.join(isinstance(a, basestring) and a or repr(a) for a in args))
69+
70+
71+
class ScanCache(object):
72+
"""
73+
A file-based cache for scan results.
74+
This is NOT thread-safe, but is multi-process safe.
75+
"""
76+
def __init__(self, cache_dir):
77+
fileutils.create_dir(cache_dir)
78+
79+
# create a unique temp directory in cache_dir
80+
self.cache_base_dir = fileutils.get_temp_dir(cache_dir, prefix=timeutils.time2tstamp() + '-')
81+
82+
# and subdirs for infos and scans caches
83+
self.cache_infos_dir = os.path.join(self.cache_base_dir, 'infos')
84+
fileutils.create_dir(self.cache_infos_dir)
85+
self.cache_scans_dir = os.path.join(self.cache_base_dir, 'scans')
86+
fileutils.create_dir(self.cache_scans_dir)
87+
88+
# workaround for https://github.com/grantjenks/python-diskcache/issues/32
89+
from diskcache import Disk
90+
class DiskWithNoHighPickleProtocol(Disk):
91+
"Subclass of diskcache.Disk that always use the lowest pickle protocol."
92+
def __init__(self, directory, size_threshold, pickle_protocol):
93+
super(DiskWithNoHighPickleProtocol, self).__init__(directory, size_threshold, pickle_protocol)
94+
self._protocol = 0
95+
96+
# and finially cache instances
97+
from diskcache import Cache
98+
self.infos = Cache(self.cache_infos_dir, disk=DiskWithNoHighPickleProtocol)
99+
self.scans = Cache(self.cache_scans_dir, disk=DiskWithNoHighPickleProtocol)
100+
101+
def scan_key(self, path, file_infos):
102+
"""
103+
Return a scan cache key for a path and file_infos.
104+
"""
105+
sha1 = file_infos['sha1']
106+
# we may eventually store directories, in which case we use the path as a key
107+
return sha1 or path
108+
109+
def put_infos(self, path, file_infos):
110+
"""
111+
Put file_infos for path in the cache and return True if the file referenced
112+
in file_infos has already been scanned or False otherwise.
113+
"""
114+
self.infos.set(path, file_infos)
115+
has_cached_details = self.scan_key(path, file_infos) in self.scans
116+
if TRACE:
117+
logger_debug('put_infos:', 'path:', path, 'has_cached_details:', has_cached_details, 'file_infos:', file_infos, '\n')
118+
logger_debug('put_infos:', 'cached_infos:', self.infos[path], '\n')
119+
120+
return has_cached_details
121+
122+
def put_scan(self, path, file_infos, scan_result):
123+
"""
124+
Put scan_result in the cache. Also put file_infos in the cache if needed.
125+
"""
126+
scan_key = self.scan_key(path, file_infos)
127+
self.scans.add(scan_key, scan_result)
128+
if TRACE:
129+
logger_debug('put_scan:', 'scan_key:', scan_key, 'file_infos:', file_infos, 'scan_result:', scan_result, '\n')
130+
logger_debug('put_scan:', 'cached_infos:', self.infos[path], '\n')
131+
logger_debug('put_scan:', 'scan_key:', scan_key, 'cached_scan:', self.scans[scan_key], '\n')
132+
133+
def iterate(self, with_infos=True):
134+
"""
135+
Return an iterator of scan data for all cached scans e.g. the whole cache.
136+
"""
137+
for path in self.infos:
138+
file_infos = self.infos[path]
139+
scan_result = OrderedDict(path=path)
140+
if with_infos:
141+
# infos is always collected but only returnedd if asked:
142+
# we flatten these as direct attributes of a file object
143+
scan_result.update(file_infos.items())
144+
145+
scan_key = self.scan_key(path, file_infos)
146+
scan_details = self.scans[scan_key]
147+
scan_result.update(scan_details)
148+
if TRACE:
149+
logger_debug('iterate:', 'scan_details:', scan_details, 'for path:', path, 'scan_key:', scan_key, '\n')
150+
yield scan_result
151+
152+
def clear(self, *args):
153+
"""
154+
Purge the cache by deleting the corresponding cached data files.
155+
"""
156+
self.infos.close()
157+
self.scans.close()
158+
fileutils.delete(self.cache_base_dir)
159+
160+
161+
def get_scans_cache():
162+
return ScanCache(cache_dir=scans_cache_dir)

0 commit comments

Comments
 (0)