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