chiark / gitweb /
Add update_check_modes.txt 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     global update_stats
37     update_stats = False
38     execfile('config.py', globals())
39
40     if not update_stats:
41         print "Stats are disabled - check your configuration"
42         sys.exit(1)
43
44     # Parse command line...
45     parser = OptionParser()
46     parser.add_option("-v", "--verbose", action="store_true", default=False,
47                       help="Spew out even more information than normal")
48     parser.add_option("-d", "--download", action="store_true", default=False,
49                       help="Download logs we don't have")
50     (options, args) = parser.parse_args()
51
52     # Get all metadata-defined apps...
53     metaapps = common.read_metadata(options.verbose)
54
55     statsdir = 'stats'
56     logsdir = os.path.join(statsdir, 'logs')
57     logsarchivedir = os.path.join(logsdir, 'archive')
58     datadir = os.path.join(statsdir, 'data')
59     if not os.path.exists(statsdir):
60         os.mkdir(statsdir)
61     if not os.path.exists(logsdir):
62         os.mkdir(logsdir)
63     if not os.path.exists(datadir):
64         os.mkdir(datadir)
65
66     if options.download:
67         # Get any access logs we don't have...
68         ssh = None
69         ftp = None
70         try:
71             print 'Retrieving logs'
72             ssh = paramiko.SSHClient()
73             ssh.load_system_host_keys()
74             ssh.connect('f-droid.org', username='fdroid', timeout=10,
75                     key_filename=webserver_keyfile)
76             ftp = ssh.open_sftp()
77             ftp.get_channel().settimeout(15)
78             print "...connected"
79
80             ftp.chdir('logs')
81             files = ftp.listdir()
82             for f in files:
83                 if f.startswith('access-') and f.endswith('.log'):
84
85                     destpath = os.path.join(logsdir, f)
86                     archivepath = os.path.join(logsarchivedir, f + '.gz')
87                     if os.path.exists(archivepath):
88                         if os.path.exists(destpath):
89                             # Just in case we have it archived but failed to remove
90                             # the original...
91                             os.remove(destpath)
92                     else:
93                         destsize = ftp.stat(f).st_size
94                         if (not os.path.exists(destpath) or
95                                 os.path.getsize(destpath) != destsize):
96                             print "...retrieving " + f
97                             ftp.get(f, destpath)
98         except Exception as e:
99             traceback.print_exc()
100             sys.exit(1)
101         finally:
102             #Disconnect
103             if ftp != None:
104                 ftp.close()
105             if ssh != None:
106                 ssh.close()
107
108     # Process logs
109     logexpr = '(?P<ip>[.:0-9a-fA-F]+) - - \[(?P<time>.*?)\] "GET (?P<uri>.*?) HTTP/1.\d" (?P<statuscode>\d+) \d+ "(?P<referral>.*?)" "(?P<useragent>.*?)"'
110     logsearch = re.compile(logexpr).search
111     apps = {}
112     unknownapks = []
113     knownapks = common.KnownApks()
114     for logfile in glob.glob(os.path.join(logsdir,'access-*.log')):
115         logdate = logfile[len(logsdir) + 1 + len('access-'):-4]
116         matches = (logsearch(line) for line in file(logfile))
117         for match in matches:
118             if match and match.group('statuscode') == '200':
119                 uri = match.group('uri')
120                 if uri.endswith('.apk'):
121                     _, apkname = os.path.split(uri)
122                     app = knownapks.getapp(apkname)
123                     if app:
124                         appid, _ = app
125                         if appid in apps:
126                             apps[appid] += 1
127                         else:
128                             apps[appid] = 1
129                     else:
130                         if not apkname in unknownapks:
131                             unknownapks.append(apkname)
132
133     # Calculate and write stats for total downloads...
134     lst = []
135     alldownloads = 0
136     for app, count in apps.iteritems():
137         lst.append(app + " " + str(count))
138         alldownloads += count
139     lst.append("ALL " + str(alldownloads))
140     f = open('stats/total_downloads_app.txt', 'w')
141     f.write('# Total downloads by application, since October 2011\n')
142     for line in sorted(lst):
143         f.write(line + '\n')
144     f.close()
145
146     # Calculate and write stats for repo types...
147     repotypes = {}
148     for app in metaapps:
149         if len(app['Repo Type']) == 0:
150             rtype = 'none'
151         else:
152             rtype = app['Repo Type']
153         if rtype in repotypes:
154             repotypes[rtype] += 1;
155         else:
156             repotypes[rtype] = 1
157     f = open('stats/repotypes.txt', 'w')
158     for rtype, count in repotypes.iteritems():
159         f.write(rtype + ' ' + str(count) + '\n')
160     f.close()
161
162     # Calculate and write stats for update check modes...
163     ucms = {}
164     for app in metaapps:
165         checkmode = app['Update Check Mode']
166         if checkmode in ucms:
167             ucms[checkmode] += 1;
168         else:
169             ucms[checkmode] = 1
170     f = open('stats/update_check_modes.txt', 'w')
171     for checkmode, count in ucms.iteritems():
172         f.write(checkmode + ' ' + str(count) + '\n')
173     f.close()
174
175     # Calculate and write stats for licenses...
176     licenses = {}
177     for app in metaapps:
178         license = app['License']
179         if license in licenses:
180             licenses[license] += 1;
181         else:
182             licenses[license] = 1
183     f = open('stats/licenses.txt', 'w')
184     for license, count in licenses.iteritems():
185         f.write(license + ' ' + str(count) + '\n')
186     f.close()
187
188
189
190     # Write list of latest apps added to the repo...
191     latest = knownapks.getlatest(10)
192     f = open('stats/latestapps.txt', 'w')
193     for app in latest:
194         f.write(app + '\n')
195     f.close()
196
197     if len(unknownapks) > 0:
198         print '\nUnknown apks:'
199         for apk in unknownapks:
200             print apk
201
202     print "Finished."
203
204 if __name__ == "__main__":
205     main()
206