|
| 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