chiark / gitweb /
Added license and vcs type analysis to stats
[fdroidserver.git] / fdroidserver / stats.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # stats.py - part of the FDroid server tools
5 # Copyright (C) 2010-12, Ciaran Gultnieks, ciaran@ciarang.com
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import sys
21 import os
22 import shutil
23 import re
24 import urllib
25 import time
26 import traceback
27 import glob
28 from optparse import OptionParser
29 import HTMLParser
30 import paramiko
31 import common
32
33 def main():
34
35     # Read configuration...
36     execfile('config.py', globals())
37
38     # Parse command line...
39     parser = OptionParser()
40     parser.add_option("-v", "--verbose", action="store_true", default=False,
41                       help="Spew out even more information than normal")
42     parser.add_option("-d", "--download", action="store_true", default=False,
43                       help="Download logs we don't have")
44     (options, args) = parser.parse_args()
45
46     # Get all metadata-defined apps...
47     metaapps = common.read_metadata(options.verbose)
48
49     statsdir = 'stats'
50     logsdir = os.path.join(statsdir, 'logs')
51     logsarchivedir = os.path.join(logsdir, 'archive')
52     datadir = os.path.join(statsdir, 'data')
53     if not os.path.exists(statsdir):
54         os.mkdir(statsdir)
55     if not os.path.exists(logsdir):
56         os.mkdir(logsdir)
57     if not os.path.exists(datadir):
58         os.mkdir(datadir)
59
60     if options.download:
61         # Get any access logs we don't have...
62         ssh = None
63         ftp = None
64         try:
65             print 'Retrieving logs'
66             ssh = paramiko.SSHClient()
67             ssh.load_system_host_keys()
68             ssh.connect('f-droid.org', username='fdroid', timeout=10,
69                     key_filename=webserver_keyfile)
70             ftp = ssh.open_sftp()
71             ftp.get_channel().settimeout(15)
72             print "...connected"
73
74             ftp.chdir('logs')
75             files = ftp.listdir()
76             for f in files:
77                 if f.startswith('access-') and f.endswith('.log'):
78
79                     destpath = os.path.join(logsdir, f)
80                     archivepath = os.path.join(logsarchivedir, f + '.gz')
81                     if os.path.exists(archivepath):
82                         if os.path.exists(destpath):
83                             # Just in case we have it archived but failed to remove
84                             # the original...
85                             os.remove(destpath)
86                     else:
87                         destsize = ftp.stat(f).st_size
88                         if (not os.path.exists(destpath) or
89                                 os.path.getsize(destpath) != destsize):
90                             print "...retrieving " + f
91                             ftp.get(f, destpath)
92         except Exception as e:
93             traceback.print_exc()
94             sys.exit(1)
95         finally:
96             #Disconnect
97             if ftp != None:
98                 ftp.close()
99             if ssh != None:
100                 ssh.close()
101
102     # Process logs
103     logexpr = '(?P<ip>[.:0-9a-fA-F]+) - - \[(?P<time>.*?)\] "GET (?P<uri>.*?) HTTP/1.\d" (?P<statuscode>\d+) \d+ "(?P<referral>.*?)" "(?P<useragent>.*?)"'
104     logsearch = re.compile(logexpr).search
105     apps = {}
106     unknownapks = []
107     knownapks = common.KnownApks()
108     for logfile in glob.glob(os.path.join(logsdir,'access-*.log')):
109         logdate = logfile[len(logsdir) + 1 + len('access-'):-4]
110         matches = (logsearch(line) for line in file(logfile))
111         for match in matches:
112             if match and match.group('statuscode') == '200':
113                 uri = match.group('uri')
114                 if uri.endswith('.apk'):
115                     _, apkname = os.path.split(uri)
116                     app = knownapks.getapp(apkname)
117                     if app:
118                         appid, _ = app
119                         if appid in apps:
120                             apps[appid] += 1
121                         else:
122                             apps[appid] = 1
123                     else:
124                         if not apkname in unknownapks:
125                             unknownapks.append(apkname)
126
127     # Calculate and write stats for total downloads...
128     lst = []
129     alldownloads = 0
130     for app, count in apps.iteritems():
131         lst.append(app + " " + str(count))
132         alldownloads += count
133     lst.append("ALL " + str(alldownloads))
134     f = open('stats/total_downloads_app.txt', 'w')
135     f.write('# Total downloads by application, since October 2011\n')
136     for line in sorted(lst):
137         f.write(line + '\n')
138     f.close()
139
140     # Calculate and write stats for repo types...
141     repotypes = {}
142     for app in metaapps:
143         if len(app['Repo Type']) == 0:
144             rtype = 'none'
145         else:
146             rtype = app['Repo Type']
147         if rtype in repotypes:
148             repotypes[rtype] += 1;
149         else:
150             repotypes[rtype] = 1
151     f = open('stats/repotypes.txt', 'w')
152     for rtype, count in repotypes.iteritems():
153         f.write(rtype + ' ' + str(count) + '\n')
154     f.close()
155
156     # Calculate and write stats for licenses...
157     licenses = {}
158     for app in metaapps:
159         license = app['License']
160         if license in licenses:
161             licenses[license] += 1;
162         else:
163             licenses[license] = 1
164     f = open('stats/licenses.txt', 'w')
165     for license, count in licenses.iteritems():
166         f.write(license + ' ' + str(count) + '\n')
167     f.close()
168
169
170
171     # Write list of latest apps added to the repo...
172     latest = knownapks.getlatest(10)
173     f = open('stats/latestapps.txt', 'w')
174     for app in latest:
175         f.write(app + '\n')
176     f.close()
177
178     if len(unknownapks) > 0:
179         print '\nUnknown apks:'
180         for apk in unknownapks:
181             print apk
182
183     print "Finished."
184
185 if __name__ == "__main__":
186     main()
187