Index: trunk/extensions/SwiftMedia/SwiftMedia.i18n.php |
— | — | @@ -1,8 +0,0 @@ |
2 | | -<?php |
3 | | -$messages = array(); |
4 | | - |
5 | | -$messages['en'] = array( |
6 | | - 'swiftmedia' => 'Openstack\'s Swift is a very large scale reliable object store. It will serve multiple petabytes of media files. |
7 | | -[[Extension:SwiftMedia]] allows MediaWiki uploads to be stored in Swift.', |
8 | | -); |
9 | | - |
Index: trunk/extensions/SwiftMedia/SwiftMedia.php |
— | — | @@ -1,21 +0,0 @@ |
2 | | -<?php |
3 | | -$wgExtensionCredits['other'][] = array( |
4 | | - 'path' => __FILE__, // File name for the extension itself, required for getting the revision number from SVN - string, adding in 1.15 |
5 | | - 'name' => "SwiftMedia", // Name of extension - string |
6 | | - 'descriptionmsg' => "swiftmedia", // Same as above but name of a message, for i18n - string, added in 1.12.0 |
7 | | - 'version' => 0, // Version number of extension - number or string |
8 | | - 'author' => "Russ Nelson", // The extension author's name - string or array for multiple |
9 | | - 'url' => "http://www.mediawiki.org/wiki/Extension:SwiftMedia", // URL of extension (usually instructions) - string |
10 | | -); |
11 | | - |
12 | | -$wgAutoloadClasses['SwiftFile'] = |
13 | | - $wgAutoloadClasses['SwiftForeignDBFile'] = |
14 | | - $wgAutoloadClasses['SwiftForeignDBRepo'] = |
15 | | - $wgAutoloadClasses['SwiftForeignDBviaLBRepo'] = |
16 | | - $wgAutoloadClasses['SwiftRepo'] = dirname( __FILE__ ) . '/SwiftMedia.body.php'; |
17 | | -$wgAutoloadClasses['CF_Authentication'] = |
18 | | - $wgAutoloadClasses['CF_Connection'] = |
19 | | - $wgAutoloadClasses['CF_Container'] = |
20 | | - $wgAutoloadClasses['CF_Object'] = '/usr/share/php-cloudfiles/cloudfiles.php'; |
21 | | - |
22 | | -$wgExtensionMessagesFiles['swiftmedia'] = dirname( __FILE__ ) . '/SwiftMedia.i18n.php'; |
Index: trunk/extensions/SwiftMedia/TODO |
— | — | @@ -1,108 +0,0 @@ |
2 | | -Pending: |
3 | | - |
4 | | -This group all relate to the 404 handler: |
5 | | -6) There's no 404 handler to generate missing thumbnails. |
6 | | -7) There's no support for remote thumbnailing. |
7 | | -7.1) The SpecialUploadStash, when it calls the remote scaler, is actually just relying on the 404 handler on upload.wm.org. |
8 | | - It's just executing thumb.php (is it thumb??) to make a thumbnail on a temporary name. Note that it's using a temp/ |
9 | | - directory in the thumbs container, not a directory in the temp container. |
10 | | -7.2) The scaler cluster is simply some machines with access to the originals and thumb storage. The 404 handler running on the Apache |
11 | | - front-ends forwards the request to a thumb.php running on the scaler cluster. Thumb.php takes care of creating the thumbnail. |
12 | | -6&7) Basically, the code which currently fetches 404 thumbs from upload.wikimedia.org needs to be changed slightly so that it inserts |
13 | | - thumb.php with the appropriate parameters and fetches from the scaler cluster. |
14 | | -19) TS: "I suggest you test transform errors throughout your whole system. With our current NFS system, transform errors work by having thumb.php return an HTTP 500 response with an informative HTML error message. The error message is passed through to Squid by the 404 handler, and Squid won't cache it. The user will see a broken image icon in their browser, and choosing "view image" from the context menu of the image will show them the error message from thumb.php." |
15 | | - |
16 | | -12) Why is anybody calling resolveVirtualUrl()? It's defined in the Repo, but getPath() is defined against a file. |
17 | | -Why is UploadStashFile() being called with a virtual URL? Once the file has been stashed() it has an object name. The container name is implicit. |
18 | | -Should UploadStashFile *always* (in our case) be called with a virtual URL? |
19 | | -23) TS: "When you get around to implementing SwiftRepo::append(), it will need some sort of concurrency control to avoid having chunks overwrite each other. " |
20 | | -24) TS: "SwiftRepo::swiftcopy() should return a Status object instead of throwing exceptions." |
21 | | -26) Hazmat wonders what happens if two clients fetch missing thumbnails at the same time. What happens when you have overlapping writes to the same object. |
22 | | - |
23 | | -Partially resolved: |
24 | | - |
25 | | -20) TS: "It's not appropriate to allow CloudFiles exceptions to be propagated back to the callers of File/FileRepo methods such as transform(). Doing this will cause the page to not be displayed at all, with an exceptionally ugly error message in its place. FileRepo has a system for returning user-friendly error messages from pretty much anywhere. For example, SwiftRepo::storeBatch() has a Status object to hold error messages, but your code just sets it to a "good" result, it never sets any errors in it." |
26 | | -"You can throw an MWException in response to configuration errors, or assertion-like unexpected errors, but it's not appropriate to be throwing exceptions in response to network errors or errors generated by the Swift server. |
27 | | - |
28 | | -Resolved: |
29 | | - |
30 | | -21a) TS: "// Check overwriting |
31 | | - if (0) { #FIXME |
32 | | - |
33 | | -Fix that. It's important to avoid data loss in file rename operations." |
34 | | -22) TS: "I think this [MD65] validation feature in FSRepo is just a hack for NFS. Okay to remove it." |
35 | | -5) The Upload seems to take more time than I expect, but that could be a function of generating the six thumbnails. |
36 | | - It *is* a function of generating the seven (we generate 800x600 twice) thumbnails. Each one takes 1/2 second. |
37 | | -8) Test cases (but of course that could be done until the cows come home). |
38 | | -9) Read through the code and look for anything which is insane. |
39 | | -10) Remove directory from $wgLocalFileRepo, to make sure that there's no references to it. Ditto for wgDeletedDirectory and deletedDir. |
40 | | - wgDeletedDirectory and deletedDir can be removed. |
41 | | -11) Determine what to do about the one remaining core change needed for Swift. |
42 | | -12) Implement repo->freeTemp() - needed by several extensions and UploadFromStash. |
43 | | -13) Do we need $wgLocalRepo->ThumbUrl to be configurable given that the Python middleware presumes it? |
44 | | - We currently have no need for it to be configurable in Swift. I'll just hard-code it to .../thumb with a note saying |
45 | | - that if it gets changed here, it needs to be changed in the Swift middleware as well. |
46 | | -14) TS: "File::transform() needs to be split into two functions, one with the code that you're duplicating, and the other with the code that you're overriding." |
47 | | -15) TS: "Scripted transform needs to keep working, ... We use it for private wikis. The "transform via 404" feature needs to be left in as well." |
48 | | -16) TS: "You need to re-add an equivalent for the file_exists($thumbPath) check that's in File::transform()." |
49 | | -17) TS: "LocalFile uses the 404 handler. Swift will probably use the 404 handler. That's why you need to support canTransformVia404()." |
50 | | -18) TS: "You need to handle errors from MediaHandler::doTransform() correctly. " |
51 | | -20) TS: "SwiftMedia::migrateThumbFile() should just do nothing," |
52 | | -21) TS: "[$wgExcludeFromThumbnailPurge] looks pretty trivial to implement to me." (RN: It was!) |
53 | | -25) TS: In the short term, just copy ForeignDBViaLBRepo to ForeignDBViaSMRepo and change the class parent. |
54 | | - |
55 | | - |
56 | | - |
57 | | -neilk_: okay, the moment when an upload passes from being a temp file into something else is at $upload->processUpload() |
58 | | -neilk_: in the old design, in essence, all this does is move a file into an NFS directory, and creates the matching database entry which creates a wiki page. |
59 | | -neilk_: so far I don't think this should be news? |
60 | | -nelson____: right |
61 | | -neilk_: So that's includes/specials/SpecialUpload.php |
62 | | -neilk_: then there's includes/api/ApiUpload.php |
63 | | -neilk_: which is similar but not quite the same |
64 | | - |
65 | | -Watch for that! |
66 | | - |
67 | | -neilk_: in ApiUpload.php there is the option to stash explicitly |
68 | | -neilk_: so the path is a bit convoluted in ApiUpload.php. Also if I remember right the file is accessed a bit differently |
69 | | -neilk_: it is possible to have stashing in ALL of these cases |
70 | | -neilk_: but in SpecialUpload, stashing occurs if there's a recoverable error with the file, like a bad file name |
71 | | -neilk_: in ApiUpload, stashing can happen for that reason, or it can happens if you ask for it explicitly (which is how UploadWizard works). |
72 | | -neilk_: nelson__: anyway is this answering your question yet? |
73 | | -nelson____: yes. |
74 | | -neilk_: ok so that's the overview of the upload methods & stashing, what else |
75 | | -nelson____: I think that part of the problem is that various parts of the system feel free to make the jump from "stored" to "accessible as full path". |
76 | | -neilk_: yes |
77 | | -neilk_: it drove me nuts too |
78 | | -neilk_: and the code intentionally conflates a number of cases, because MediaWiki at heart just wants to throw a number of files into a directory, not manage millions of them |
79 | | -nelson____: I gotta figure out how to mark the difference, so that something is either 1) a locally stored file, or 2) a blind token from the repo. |
80 | | -neilk_: can't you subclass FileRepo then? |
81 | | -nelson____: cuz if you have the blind token, then you need to turn it into a File and then call getPath() on it. |
82 | | -nelson____: but there's times when the upload code expects to be able to access a file without creating a File first. |
83 | | -neilk_: when does upload code access a file that isn't a File? |
84 | | -nelson____: sec |
85 | | -nelson____: neilk_: UploadStashFile does this in its __construct: $path = $repo->resolveVirtualUrl( $path ); |
86 | | -neilk_: yes |
87 | | -nelson____: But maybe the key thing for me to know is that when it's SwiftMedia, $path on the right is *always* a mwrepo/ |
88 | | -nelson____: If that's the case, then I think I'm okay. I'm just having trouble following the code up and out and then back down. |
89 | | -neilk_: there isn't any code, to my knowledge, which assumes that $path is a "physical" path. It uses the repo methods. |
90 | | -neilk_: I don't blame you if you're having trouble. |
91 | | -neilk_: isn't any code in UploadStashFile, I mean. |
92 | | -nelson____: maybe ... what I should do is throw an exception if SwiftRepo::resolveVirtualUrl ever gets called without a mwrepo url, and then just go test everything. |
93 | | -nelson____: I think maybe I'm trying to overanalyze the code. |
94 | | -neilk_: I sympathize |
95 | | -nelson____: I should trust the code more. |
96 | | -neilk_: hm, I think not |
97 | | -nelson____: Trust but verify. |
98 | | -neilk_: also, this is sad but stashing is done in two slightly different ways, too. |
99 | | -nelson____: I saw. |
100 | | -neilk_: but compatible |
101 | | -neilk_: I wanted UploadStash to absorb the other one. |
102 | | -neilk_: We can still do that. |
103 | | -nelson____: agreed. |
104 | | -neilk_: When I was in the middle of Upload code, I always felt like I was cramped in some access area between two walls, with all the pipes and electrical work. |
105 | | -nelson____: interesting metaphor. |
106 | | -nelson____: yeah, I think part of that problem is you're always stuck between the database and the filestore. |
107 | | -nelson____: they both have opinions about how things work, and you have to keep them consistent. |
108 | | -nelson____: Okay, I'm gonna take some notes then go home. taking the weekend off for a bicycle road trip. |
109 | | - |
Index: trunk/extensions/SwiftMedia/smtest.py |
— | — | @@ -1,169 +0,0 @@ |
2 | | -#!/usr/bin/python |
3 | | -# http://www.deheus.net/petrik/blog/2005/11/20/creating-a-wikipedia-watchlist-rss-feed-with-python-and-twill/ |
4 | | - |
5 | | -import sys, string, datetime, time, os, re, stat |
6 | | -import twill |
7 | | -import twill.commands as t |
8 | | -import gd |
9 | | - |
10 | | -temp_html = "/tmp/wikipedia.html" |
11 | | -rss_title = "Wikipedia watchlist" |
12 | | -rss_link = "http://en.wikipedia.org" |
13 | | -host = "http://ersch.wikimedia.org/" |
14 | | -#host = "http://127.0.0.1/wiki/" |
15 | | - |
16 | | -def login(username, password): |
17 | | - t.add_extra_header("User-Agent", "python-twill-russnelson@gmail.com") |
18 | | - |
19 | | - t.go(host+"index.php/Special:UserLogin") |
20 | | - t.fv("1", "wpName", username) |
21 | | - t.fv("1", "wpPassword", password) |
22 | | - t.submit("wpLoginAttempt") |
23 | | - |
24 | | - |
25 | | -def upload_list(browser, pagename, uploads): |
26 | | - |
27 | | - # get the file sizes for later comparison. |
28 | | - filesizes = [] |
29 | | - for fn in uploads: |
30 | | - filesizes.append(os.stat(fn)[stat.ST_SIZE]) |
31 | | - filesizes.reverse() # because they get listed newest first. |
32 | | - |
33 | | - # Upload copy #1. |
34 | | - t.go(host+"index.php/Special:Upload") |
35 | | - t.formfile("1", "wpUploadFile", uploads[0]) |
36 | | - t.fv("1", "wpDestFile", pagename) |
37 | | - t.fv("1", "wpUploadDescription", "Uploading %s" % pagename) |
38 | | - t.submit("wpUpload") |
39 | | - |
40 | | - # Verify that we succeeded. |
41 | | - t.find("File:%s" % pagename) |
42 | | - |
43 | | - for fn in uploads[1:]: |
44 | | - # propose that we upload a replacement |
45 | | - t.go(host+"index.php?title=Special:Upload&wpDestFile=%s&wpForReUpload=1" % pagename) |
46 | | - t.formfile("1", "wpUploadFile", fn) |
47 | | - t.fv("1", "wpUploadDescription", "Uploading %s as %s" % (fn, pagename)) |
48 | | - t.submit("wpUpload") |
49 | | - |
50 | | - # get the URLs for the thumbnails |
51 | | - urls = [] |
52 | | - for url in re.finditer(r'<td><a href="([^"]*?)"><img alt="Thumbnail for version .*?" src="(.*?)"', browser.get_html()): |
53 | | - urls.append(url.group(1)) |
54 | | - urls.append(url.group(2)) |
55 | | - |
56 | | - print filesizes |
57 | | - for i, url in enumerate(urls): |
58 | | - t.go(url) |
59 | | - if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]: |
60 | | - print i,len(browser.get_html()), filesizes[i / 2] |
61 | | - t.find("Files differ in size") |
62 | | - t.code("200") |
63 | | - t.back() |
64 | | - |
65 | | - # delete all versions |
66 | | - t.go(host+"index.php?title=File:%s&action=delete" % pagename) |
67 | | - # after we get the confirmation page, commit to the action. |
68 | | - t.fv("1", "wpReason", "Test Deleting...") |
69 | | - t.submit("mw-filedelete-submit") |
70 | | - |
71 | | - # make sure that we can't visit their URLs. |
72 | | - for i, url in enumerate(urls): |
73 | | - t.go(url) |
74 | | - if 0 and i % 2 == 1 and i > 0 and browser.get_code() == 200: |
75 | | - # bug 30192: the archived file's thumbnail doesn't get deleted. |
76 | | - print "special-casing the last URL" |
77 | | - continue |
78 | | - t.code("404") |
79 | | - |
80 | | - # restore the current and archived version. |
81 | | - t.go(host+"index.php/Special:Undelete/File:%s" % pagename) |
82 | | - t.fv("1", "wpComment", "Test Restore") |
83 | | - t.submit("restore") |
84 | | - |
85 | | - # visit the page to make sure that the thumbs get re-rendered properly. |
86 | | - # when we get the 404 handler working correctly, this won't be needed. |
87 | | - t.go(host+"index.php?title=File:%s" % pagename) |
88 | | - |
89 | | - # make sure that they got restored correctly. |
90 | | - for i, url in enumerate(urls): |
91 | | - t.go(url) |
92 | | - if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]: |
93 | | - t.find("Files differ in size") |
94 | | - t.code("200") |
95 | | - t.back() |
96 | | - |
97 | | - if len(uploads) != 2: |
98 | | - return |
99 | | - |
100 | | - match = re.search(r'"([^"]+?)" title="[^"]+?">revert', browser.get_html()) |
101 | | - if not match: |
102 | | - t.find('revert') |
103 | | - t.go(match.group(1).replace('&', '&')) |
104 | | - |
105 | | -def make_files(pagename): |
106 | | - redfilename = "/tmp/Red-%s" % pagename |
107 | | - greenfilename = "/tmp/Green-%s" % pagename |
108 | | - bluefilename = "/tmp/Blue-%s" % pagename |
109 | | - |
110 | | - # create a small test image. |
111 | | - gd.gdMaxColors = 256 |
112 | | - i = gd.image((200,100)) |
113 | | - black = i.colorAllocate((0,0,0)) |
114 | | - white = i.colorAllocate((255,255,255)) |
115 | | - red = i.colorAllocate((255,55,55)) |
116 | | - green = i.colorAllocate((55,255,55)) |
117 | | - blue = i.colorAllocate((55,55,255)) |
118 | | - |
119 | | - # now write a red version |
120 | | - i.rectangle((0,0),(199,99),red, red) |
121 | | - i.line((0,0),(199,99),black) |
122 | | - i.string(gd.gdFontLarge, (5,50), pagename, white) |
123 | | - i.writePng(redfilename) |
124 | | - |
125 | | - # now write a green version |
126 | | - i.rectangle((0,0),(199,99),green, green) |
127 | | - i.line((0,0),(99,99),black) |
128 | | - i.string(gd.gdFontLarge, (5,50), pagename, white) |
129 | | - i.writePng(greenfilename) |
130 | | - |
131 | | - # write a blue version |
132 | | - i.rectangle((0,0),(199,99),blue,blue) |
133 | | - i.line((0,0),(99,199),black) |
134 | | - i.string(gd.gdFontLarge, (5,50), pagename, white) |
135 | | - i.writePng(bluefilename) |
136 | | - |
137 | | - # propose that we delete it (in case it exists) |
138 | | - t.go(host+"index.php?title=File:%s&action=delete" % pagename) |
139 | | - # make sure that we've NOT gotten the wrong page and HAVE gotten the right one. |
140 | | - t.notfind('You are about to delete the file') |
141 | | - t.find("could not be deleted") |
142 | | - |
143 | | - return (redfilename, greenfilename, bluefilename ) |
144 | | - |
145 | | -def main(): |
146 | | - try: |
147 | | - username = sys.argv[1] |
148 | | - password = sys.argv[2] |
149 | | - except IndexError: |
150 | | - print "Please supply username password" |
151 | | - sys.exit(1) |
152 | | - browser = twill.get_browser() |
153 | | - login(username, password) |
154 | | - |
155 | | - serial = time.time() |
156 | | - pagename = "Test-%s.png" % serial |
157 | | - filenames = make_files(pagename) |
158 | | - upload_list(browser, pagename, filenames[0:2]) |
159 | | - |
160 | | - # try it again with two replacement files. |
161 | | -# pagename = "Test-%sA.png" % serial |
162 | | -# filenames = make_files(pagename) |
163 | | -# upload_list(browser, pagename, filenames) |
164 | | - |
165 | | - t.showforms() |
166 | | - t.save_html("/tmp/testabcd") |
167 | | - |
168 | | -if __name__ == "__main__": |
169 | | - main() |
170 | | - |
Index: trunk/extensions/SwiftMedia/test_rewrite.py |
— | — | @@ -1,171 +0,0 @@ |
2 | | -#!/usr/bin/python |
3 | | - |
4 | | -import unittest |
5 | | - |
6 | | -import webob |
7 | | - |
8 | | -from wmf import rewrite |
9 | | -from wmf.client import ClientException |
10 | | - |
11 | | -class FakeApp(object): |
12 | | - def __init__(self, status, headers): |
13 | | - self.status = status |
14 | | - self.headers = headers |
15 | | - |
16 | | - def __call__(self, env, start_response): |
17 | | - start_response(self.status, self.headers) |
18 | | - return "FAKE APP" |
19 | | - |
20 | | -def start_response(*args): |
21 | | - pass |
22 | | - |
23 | | -class TestRewrite(unittest.TestCase): |
24 | | - |
25 | | - def setUp(self): |
26 | | - pass |
27 | | - |
28 | | - account="AUTH_..." |
29 | | - urlbig = 'http://alsted.wikimedia.org/wikipedia/commons/a/aa/'\ |
30 | | - 'Dzimbo_u_Beogradu_19.jpeg' |
31 | | - urlorig = '/v1/' + account + \ |
32 | | - '/wikipedia%2Fcommons/a/aa/Dzimbo_u_Beogradu_19.jpeg' |
33 | | - thumbbig = 'http://alsted.wikimedia.org/wikipedia/commons/thumb/a/aa/'\ |
34 | | - 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg' |
35 | | - url448 = '/v1/' + account + \ |
36 | | - '/wikipedia%2Fcommons%2Fthumb/a/aa/'\ |
37 | | - 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg' |
38 | | - urlaccount = 'http://alsted.wikimedia.org/' + account |
39 | | - contname = '/wikipedia/commons/thumb' |
40 | | - objname = '/a/aa/Dzimbo_u_Beogradu_19.jpeg/91px-Dzimbo_u_Beogradu_19.jpeg' |
41 | | - a = dict(account=account, |
42 | | - url="https://127.0.0.1:11000/v1.0", |
43 | | - login="yourlogin", |
44 | | - thumbhost='localhost', |
45 | | - user_agent='Mozilla/5.0', |
46 | | - key="yourkey") |
47 | | - |
48 | | - def test_01(self): |
49 | | - """#01 Cur controller can snarf its args.""" |
50 | | - controller = rewrite.ObjectController() |
51 | | - controller.do_start_response("200 Good", {"test": "testy"}) |
52 | | - self.assertEquals(controller.response_args[0], "200 Good") |
53 | | - self.assertEquals(controller.response_args[1], {"test": "testy"}) |
54 | | - |
55 | | - def test_01a(self): |
56 | | - """#01a Our app calls into the FakeApp; returns its results if 200 """ |
57 | | - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
58 | | - req = webob.Request.blank(self.urlbig, |
59 | | - environ={'REQUEST_METHOD': 'GET'}) |
60 | | - controller = rewrite.ObjectController() |
61 | | - resp = app(req.environ, controller.do_start_response) |
62 | | - self.assertEquals(resp, 'FAKE APP') |
63 | | - |
64 | | - def test_02(self): |
65 | | - """#02 Test URL rewriting for originals. """ |
66 | | - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
67 | | - req = webob.Request.blank(self.urlbig, |
68 | | - environ={'REQUEST_METHOD': 'GET'}) |
69 | | - resp = app(req.environ, start_response) |
70 | | - self.assertEquals(req.environ['PATH_INFO'], self.urlorig) |
71 | | - |
72 | | - def test_03(self): |
73 | | - """#03 Test URL rewriting for thumbs. """ |
74 | | - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
75 | | - req = webob.Request.blank(self.thumbbig, |
76 | | - environ={'REQUEST_METHOD': 'GET'}) |
77 | | - resp = app(req.environ, start_response) |
78 | | - self.assertEquals(req.environ['PATH_INFO'], self.url448) |
79 | | - |
80 | | - def test_04(self): |
81 | | - """#04 Test a write. Could fail if our token has gone stale""" |
82 | | - app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a) |
83 | | - req = webob.Request.blank(self.thumbbig, |
84 | | - environ={'REQUEST_METHOD': 'GET'}) |
85 | | - resp = app(req.environ, start_response) |
86 | | - self.assertEquals(req.environ['PATH_INFO'], self.url448) |
87 | | - # note that we PUT this file onto the server here, even if it's already there. |
88 | | - datalen = 0 |
89 | | - for data in resp: |
90 | | - datalen += len(data) |
91 | | - self.assertEquals(datalen, 51543) |
92 | | - |
93 | | - def test_05(self): |
94 | | - """#05 Report 401 (authorization) errors""" |
95 | | - app = rewrite.WMFRewrite(FakeApp("401 Bad", {}),self.a) |
96 | | - req = webob.Request.blank(self.thumbbig, |
97 | | - environ={'REQUEST_METHOD': 'GET'}) |
98 | | - resp = app(req.environ, start_response) |
99 | | - self.assertEquals(req.environ['PATH_INFO'], self.url448) |
100 | | - self.assertEquals(resp, |
101 | | - ['401 Unauthorized\n\nThis server could not verify that you are '\ |
102 | | - 'authorized to access the document you requested. Either you '\ |
103 | | - 'supplied the wrong credentials (e.g., bad password), or your '\ |
104 | | - 'browser does not understand how to supply the credentials '\ |
105 | | - 'required.\n\n Token may have timed out ']) |
106 | | - |
107 | | - def test_06(self): |
108 | | - """#06 Give them a bad token so that the PUT fails.""" |
109 | | - app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a) |
110 | | - app.token = "HaHaYeahRight" |
111 | | - req = webob.Request.blank(self.thumbbig, |
112 | | - environ={'REQUEST_METHOD': 'GET'}) |
113 | | - resp = app(req.environ, start_response) |
114 | | - self.assertEquals(req.environ['PATH_INFO'], self.url448) |
115 | | - # the PUT fails because we give them a bad token, but ... we should |
116 | | - # really silently just hand back the file. |
117 | | - try: |
118 | | - datalen = 0 |
119 | | - for data in resp: |
120 | | - datalen += len(data) |
121 | | - except ClientException, x: |
122 | | - self.assertEquals(datalen, 51543) |
123 | | - y = "ClientException('Object PUT failed',)" |
124 | | - self.assertEquals(`x`, y) |
125 | | - |
126 | | - def test_07(self): |
127 | | - """#07 Make sure that an already-authorized path goes unchanged.""" |
128 | | - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
129 | | - url = self.urlaccount + self.contname + self.objname |
130 | | - req = webob.Request.blank(url, environ={'REQUEST_METHOD': 'GET'}) |
131 | | - resp = app(req.environ, start_response) |
132 | | - self.assertEquals(req.url, url) # should remain unchanged |
133 | | - #self.assertTrue(len(app.response_args) == 0) # no args either. |
134 | | - |
135 | | - def test_08(self): |
136 | | - """#08 Don't let them read the container""" |
137 | | - app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
138 | | - req = webob.Request.blank( |
139 | | - 'http://alsted.wikimedia.org/wikipedia/commons/', |
140 | | - environ={'REQUEST_METHOD': 'GET'}) |
141 | | - resp = app(req.environ, start_response) |
142 | | - self.assertEquals(resp, |
143 | | - ['403 Forbidden\n\nAccess was denied to this resource.\n\n '\ |
144 | | - 'No container listing ']) |
145 | | - |
146 | | - # test_09 became obsolete |
147 | | - |
148 | | - def test_10(self): |
149 | | - """#10 Trap weird-ass errors""" |
150 | | - app = rewrite.WMFRewrite(FakeApp("999 Unrecognized", {}),self.a) |
151 | | - req = webob.Request.blank("http://localhost/a/b/c", |
152 | | - environ={'REQUEST_METHOD': 'GET'}) |
153 | | - resp = app(req.environ, start_response) |
154 | | - self.assertEquals(resp, |
155 | | - ['501 Not Implemented\n\nThe server has either erred or is '\ |
156 | | - 'incapable of performing the requested operation.\n\n Unknown '\ |
157 | | - 'Status: 999 ']) |
158 | | - |
159 | | - def test_11(self): |
160 | | - """#11 Trap URLs that don't match the regexp""" |
161 | | - app = rewrite.WMFRewrite(FakeApp("404 TryRegexp", {}),self.a) |
162 | | - req = webob.Request.blank("http://localhost/a", |
163 | | - environ={'REQUEST_METHOD': 'GET'}) |
164 | | - resp = app(req.environ, start_response) |
165 | | - self.assertEquals(resp, |
166 | | - ['400 Bad Request\n\nThe server could not comply with the '\ |
167 | | - 'request since it is either malformed or otherwise '\ |
168 | | - 'incorrect.\n\n Regexp failed: "/a" ']) |
169 | | - |
170 | | -if __name__ == '__main__': |
171 | | - unittest.main() |
172 | | - |
Index: trunk/extensions/SwiftMedia/SwiftMedia.body.php |
— | — | @@ -1,1444 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * Local file in the wiki's own database, only stored in Swift |
5 | | - * |
6 | | - * @file |
7 | | - * @ingroup FileRepo |
8 | | - */ |
9 | | - |
10 | | -/** |
11 | | - * Class to represent a local file in the wiki's own database, only stored in Swift |
12 | | - * |
13 | | - * Provides methods to retrieve paths (physical, logical, URL), |
14 | | - * to generate image thumbnails or for uploading. |
15 | | - * |
16 | | - * Note that only the repo object knows what its file class is called. You should |
17 | | - * never name a file class explictly outside of the repo class. Instead use the |
18 | | - * repo's factory functions to generate file objects, for example: |
19 | | - * |
20 | | - * RepoGroup::singleton()->getLocalRepo()->newFile($title); |
21 | | - * |
22 | | - * The convenience functions wfLocalFile() and wfFindFile() should be sufficient |
23 | | - * in most cases. |
24 | | - * |
25 | | - * @ingroup FileRepo |
26 | | - */ |
27 | | -class SwiftFile extends LocalFile { |
28 | | - /**#@+ |
29 | | - * @private |
30 | | - */ |
31 | | - var |
32 | | - $conn, # our connection to the Swift proxy. |
33 | | - $fileExists, # does the file file exist on disk? (loadFromXxx) |
34 | | - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) |
35 | | - $swiftuser, |
36 | | - $swiftkey, |
37 | | - $authurl, |
38 | | - $container; |
39 | | - /**#@-*/ |
40 | | - |
41 | | - /** |
42 | | - * Create a LocalFile from a title |
43 | | - * Do not call this except from inside a repo class. |
44 | | - * |
45 | | - * Note: $unused param is only here to avoid an E_STRICT |
46 | | - * |
47 | | - * @return SwiftFile |
48 | | - */ |
49 | | - static function newFromTitle( $title, $repo, $unused = null ) { |
50 | | - if ( empty( $title ) ) { |
51 | | - return null; |
52 | | - } |
53 | | - return new self( $title, $repo ); |
54 | | - } |
55 | | - |
56 | | - /** |
57 | | - * Create a LocalFile from a title |
58 | | - * Do not call this except from inside a repo class. |
59 | | - */ |
60 | | - static function newFromRow( $row, $repo ) { |
61 | | - $title = Title::makeTitle( NS_FILE, $row->img_name ); |
62 | | - $file = new self( $title, $repo ); |
63 | | - $file->loadFromRow( $row ); |
64 | | - |
65 | | - return $file; |
66 | | - } |
67 | | - |
68 | | - /** |
69 | | - * Constructor. |
70 | | - * Do not call this except from inside a repo class. |
71 | | - */ |
72 | | - function __construct( $title, $repo ) { |
73 | | - if ( !is_object( $title ) ) { |
74 | | - throw new MWException( __CLASS__ . ' constructor given bogus title.' ); |
75 | | - } |
76 | | - |
77 | | - parent::__construct( $title, $repo ); |
78 | | - |
79 | | - $this->tempPaths = array(); // Hash from rel to local copy. |
80 | | - } |
81 | | - |
82 | | - /** splitMime inherited */ |
83 | | - /** getName inherited */ |
84 | | - /** getTitle inherited */ |
85 | | - /** getURL inherited */ |
86 | | - /** getViewURL inherited */ |
87 | | - /** isVisible inherited */ |
88 | | - |
89 | | - /** |
90 | | - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy. |
91 | | - * |
92 | | - * @return string Path to a local copy of the file. |
93 | | - */ |
94 | | - public function getPath() { |
95 | | - if ( !array_key_exists( '', $this->tempPaths ) ) { |
96 | | - $this->tempPaths[''] = $this->repo->getLocalCopy( $this->repo->container, $this->getRel(), "getPath_" ); |
97 | | - } |
98 | | - return $this->tempPaths['']; |
99 | | - } |
100 | | - |
101 | | - /** |
102 | | - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy. |
103 | | - * Get a local copy of a particular archived file specified by $suffix |
104 | | - * |
105 | | - * @param string suffix Specific archived copy. |
106 | | - * @return string Path to a local copy of the file. |
107 | | - */ |
108 | | - public function getArchivePath( $suffix = false ) { |
109 | | - if ( !$suffix ) { |
110 | | - throw new MWException( "Can't call getArchivePath without a suffix" ); |
111 | | - } |
112 | | - $rel = $this->getArchiveRel( $suffix ); |
113 | | - if ( !array_key_exists( $rel, $this->tempPaths ) ) { |
114 | | - $this->tempPaths[$rel] = $this->repo->getLocalCopy( $this->repo->container, $rel ); |
115 | | - } |
116 | | - return $this->tempPaths[$rel]; |
117 | | - } |
118 | | - |
119 | | - /** |
120 | | - * We're re-purposing getPath() to checkout a copy of the file, if we don't already have a copy. |
121 | | - * Get a local copy of a particular thumb specified by $suffix |
122 | | - * |
123 | | - * @param string suffix Specific thumbnail. |
124 | | - * @return string Path to a local copy of the file. |
125 | | - */ |
126 | | - public function getThumbPath( $suffix = false ) { |
127 | | - if ( !$suffix ) { |
128 | | - throw new MWException( "Can't call getThumbPath without a suffix" ); |
129 | | - } |
130 | | - $rel = $this->getRel() . '/' . $suffix; |
131 | | - if ( !array_key_exists( $rel, $this->tempPaths ) ) { |
132 | | - $this->tempPaths[$rel] = $this->repo->getLocalCopy( $this->repo->getZoneContainer( 'thumb' ), $rel ); |
133 | | - } |
134 | | - return $this->tempPaths[$rel]; |
135 | | - } |
136 | | - |
137 | | - function __destruct() { |
138 | | - foreach ( $this->tempPaths as $path ) { |
139 | | - // Clean up temporary data. |
140 | | - wfDebug( __METHOD__ . ": deleting $path\n" ); |
141 | | - unlink( $path ); |
142 | | - } |
143 | | - $this->tempPaths = array(); |
144 | | - } |
145 | | - |
146 | | - /** |
147 | | - * Do the work of a transform (from an original into a thumb). |
148 | | - * Contains filesystem-specific functions. |
149 | | - * |
150 | | - * @param $thumbName string: the name of the thumbnail file. |
151 | | - * @param $thumbUrl string: the URL of the thumbnail file. |
152 | | - * @param $params Array: an associative array of handler-specific parameters. |
153 | | - * Typical keys are width, height and page. |
154 | | - * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering |
155 | | - * |
156 | | - * @return MediaTransformOutput | false |
157 | | - */ |
158 | | - function maybeDoTransform( $thumbName, $thumbUrl, $params, $flags ) { |
159 | | - global $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgTmpDirectory; |
160 | | - |
161 | | - // get a temporary place to put the original. |
162 | | - $thumbPath = tempnam( $wgTmpDirectory, 'transform_out_' ); |
163 | | - unlink( $thumbPath ); |
164 | | - $thumbPath .= '.' . pathinfo( $thumbName, PATHINFO_EXTENSION ); |
165 | | - |
166 | | - |
167 | | - if ( $this->repo && $this->repo->canTransformVia404() && !( $flags & self::RENDER_NOW ) ) { |
168 | | - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); |
169 | | - } |
170 | | - |
171 | | - // see if the file exists, and if it exists, is not too old. |
172 | | - $conn = $this->repo->connect(); |
173 | | - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' ); |
174 | | - try { |
175 | | - $pic = $container->get_object( $this->getRel() . "/$thumbName" ); |
176 | | - } catch ( NoSuchObjectException $e ) { |
177 | | - $pic = NULL; |
178 | | - } |
179 | | - if ( $pic ) { |
180 | | - $thumbTime = $pic->last_modified; |
181 | | - $tm = strptime( $thumbTime, '%a, %d %b %Y %H:%M:%S GMT' ); |
182 | | - $thumbGMT = gmmktime( $tm['tm_hour'], $tm['tm_min'], $tm['tm_sec'], $tm['tm_mon'] + 1, $tm['tm_mday'], $tm['tm_year'] + 1900 ); |
183 | | - wfDebug( __METHOD__ . ": $thumbName is dated $thumbGMT\n" ); |
184 | | - if ( gmdate( 'YmdHis', $thumbGMT ) >= $wgThumbnailEpoch ) { |
185 | | - |
186 | | - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); |
187 | | - } |
188 | | - } |
189 | | - $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); |
190 | | - |
191 | | - // Ignore errors if requested |
192 | | - if ( !$thumb ) { |
193 | | - $thumb = null; |
194 | | - } elseif ( $thumb->isError() ) { |
195 | | - $this->lastError = $thumb->toText(); |
196 | | - if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) { |
197 | | - $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); |
198 | | - } |
199 | | - } |
200 | | - |
201 | | - // what if they didn't actually write out a thumbnail? Check the file size. |
202 | | - if ( $thumb && file_exists( $thumbPath ) && filesize( $thumbPath ) ) { |
203 | | - // Store the thumbnail into Swift, but in the thumb version of the container. |
204 | | - wfDebug( __METHOD__ . ': creating thumb ' . $this->getRel() . "/$thumbName\n" ); |
205 | | - $this->repo->write_swift_object( $thumbPath, $container, $this->getRel() . "/$thumbName" ); |
206 | | - // php-cloudfiles throws exceptions, so failure never gets here. |
207 | | - } |
208 | | - |
209 | | - // Clean up temporary data, if it exists. |
210 | | - if ( file_exists( $thumbPath ) ) { |
211 | | - wfSuppressWarnings(); |
212 | | - unlink( $thumbPath ); |
213 | | - wfRestoreWarnings(); |
214 | | - } |
215 | | - |
216 | | - return $thumb; |
217 | | - } |
218 | | - |
219 | | - /** transform inherited */ |
220 | | - |
221 | | - /** |
222 | | - * We have nothing to do here. |
223 | | - */ |
224 | | - function migrateThumbFile( $thumbName ) { |
225 | | - return; |
226 | | - } |
227 | | - /** |
228 | | - * Get the public root directory of the repository. |
229 | | - */ |
230 | | - protected function getRootDirectory() { |
231 | | - throw new MWException( __METHOD__ . ': not implemented' ); |
232 | | - } |
233 | | - |
234 | | - /** getHandler inherited */ |
235 | | - /** iconThumb inherited */ |
236 | | - /** getLastError inherited */ |
237 | | - |
238 | | - /** |
239 | | - * Get all thumbnail names previously generated for this file |
240 | | - * @param $archiveName string: the article name for the archived file (if archived). |
241 | | - * @return a list of files, the first entry of which is the directory name (if applicable). |
242 | | - */ |
243 | | - function getThumbnails( $archiveName = false ) { |
244 | | - $this->load(); |
245 | | - |
246 | | - if ( $archiveName ) { |
247 | | - $prefix = $this->getArchiveRel( $archiveName ); |
248 | | - } else { |
249 | | - $prefix = $this->getRel(); |
250 | | - } |
251 | | - $conn = $this->repo->connect(); |
252 | | - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' ); |
253 | | - $files = $container->list_objects( 0, NULL, $prefix ); |
254 | | - array_unshift( $files, 'unused' ); # return an unused $dir. |
255 | | - return $files; |
256 | | - } |
257 | | - |
258 | | - /** |
259 | | - * Delete cached transformed files |
260 | | - * @param $dir string Should always be the 'unused' we specified earlier. |
261 | | - * @param $files array of strings listing the thumbs to be deleted. |
262 | | - */ |
263 | | - function purgeThumbList( $dir, $files ) { |
264 | | - global $wgExcludeFromThumbnailPurge; |
265 | | - |
266 | | - $conn = $this->repo->connect(); |
267 | | - $container = $this->repo->get_container( $conn, $this->repo->container . '-thumb' ); |
268 | | - foreach ( $files as $file ) { |
269 | | - // Only remove files not in the $wgExcludeFromThumbnailPurge configuration variable |
270 | | - $ext = pathinfo( $file, PATHINFO_EXTENSION ); |
271 | | - if ( in_array( $ext, $wgExcludeFromThumbnailPurge ) ) { |
272 | | - continue; |
273 | | - } |
274 | | - |
275 | | - wfDebug( __METHOD__ . ' deleting ' . $container->name . "/$file\n" ); |
276 | | - $this->repo->swift_delete( $container, $file ); |
277 | | - } |
278 | | - } |
279 | | - |
280 | | -} // SwiftFile class |
281 | | - |
282 | | -# ------------------------------------------------------------------------------ |
283 | | - |
284 | | -/** |
285 | | - * Repository that stores files in Swift and registers them |
286 | | - * in the wiki's own database. |
287 | | - * |
288 | | - * @file |
289 | | - * @ingroup FileRepo |
290 | | - */ |
291 | | - |
292 | | -class SwiftRepo extends LocalRepo { |
293 | | - // The public interface to SwiftFile is through SwiftRepo's findFile and |
294 | | - // newFile. They call into the repo's NewFile and FindFile, which call |
295 | | - // one of these factories to create the File object. |
296 | | - var $fileFactory = array( 'SwiftFile', 'newFromTitle' ); |
297 | | - var $fileFactoryKey = array( 'SwiftFile', 'newFromKey' ); |
298 | | - var $fileFromRowFactory = array( 'SwiftFile', 'newFromRow' ); |
299 | | - var $oldFileFactory = array( 'OldSwiftFile', 'newFromTitle' ); |
300 | | - var $oldFileFactoryKey = array( 'OldSwiftFile', 'newFromKey' ); |
301 | | - var $oldFileFromRowFactory = array( 'OldSwiftFile', 'newFromRow' ); |
302 | | - |
303 | | - function __construct( $info ) { |
304 | | - // We don't call parent::_construct because it requires $this->directory, |
305 | | - // which doesn't exist in Swift. |
306 | | - FileRepo::__construct( $info ); |
307 | | - |
308 | | - // Required settings |
309 | | - $this->url = $info['url']; |
310 | | - |
311 | | - // Optional settings |
312 | | - $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2; |
313 | | - $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? |
314 | | - $info['deletedHashLevels'] : $this->hashLevels; |
315 | | - |
316 | | - // This relationship is also hard-coded in rewrite.py, another part of this |
317 | | - // extension. If you want to change this here, you might have to change it |
318 | | - // there, too. |
319 | | - $this->thumbUrl = "{$this->url}/thumb"; |
320 | | - |
321 | | - // we don't have directories |
322 | | - $this->deletedDir = false; |
323 | | - |
324 | | - // Required settings |
325 | | - $this->swiftuser = $info['user']; |
326 | | - $this->swiftkey = $info['key']; |
327 | | - $this->authurl = $info['authurl']; |
328 | | - $this->container = $info['container']; |
329 | | - } |
330 | | - |
331 | | - /** |
332 | | - * Get a connection to the swift proxy. |
333 | | - * |
334 | | - * @return CF_Connection |
335 | | - */ |
336 | | - function connect() { |
337 | | - $auth = new CF_Authentication( $this->swiftuser, $this->swiftkey, NULL, $this->authurl ); |
338 | | - try { |
339 | | - $auth->authenticate(); |
340 | | - } catch ( AuthenticationException $e ) { |
341 | | - throw new MWException( "We can't authenticate ourselves." ); |
342 | | - # } catch (InvalidResponseException $e) { |
343 | | - # throw new MWException( __METHOD__ . "unexpected response '$e'" ); |
344 | | - } |
345 | | - return new CF_Connection( $auth ); |
346 | | - } |
347 | | - |
348 | | - /** |
349 | | - * Given a connection and container name, return the container. |
350 | | - * We KNOW the container should exist, so puke if it doesn't. |
351 | | - * |
352 | | - * @param $conn CF_Connection |
353 | | - * |
354 | | - * @return CF_Container |
355 | | - */ |
356 | | - function get_container( $conn, $cont ) { |
357 | | - try { |
358 | | - return $conn->get_container( $cont ); |
359 | | - } catch ( NoSuchContainerException $e ) { |
360 | | - throw new MWException( "A container we thought existed, doesn't." ); |
361 | | - # } catch (InvalidResponseException $e) { |
362 | | - # throw new MWException( __METHOD__ . "unexpected response '$e'" ); |
363 | | - } |
364 | | - } |
365 | | - |
366 | | - /** |
367 | | - * Given a filename, container, and object name, write the file into the object. |
368 | | - * None of these error conditions are recoverable by the user, so we just dump |
369 | | - * an Internal Error on them. |
370 | | - * |
371 | | - * @return CF_Container |
372 | | - */ |
373 | | - function write_swift_object( $srcPath, $dstc, $dstRel ) { |
374 | | - try { |
375 | | - $obj = $dstc->create_object( $dstRel ); |
376 | | - $obj->load_from_filename( $srcPath, True ); |
377 | | - } catch ( SyntaxException $e ) { |
378 | | - throw new MWException( 'missing required parameters' ); |
379 | | - } catch ( BadContentTypeException $e ) { |
380 | | - throw new MWException( 'No Content-Type was/could be set' ); |
381 | | - # } catch (InvalidResponseException $e) { |
382 | | - # throw new MWException( __METHOD__ . "unexpected response '$e'" ); |
383 | | - } catch ( IOException $e ) { |
384 | | - throw new MWException( "error opening file '$e'" ); |
385 | | - } |
386 | | - } |
387 | | - |
388 | | - /** |
389 | | - * Given a container and object name, delete the object. |
390 | | - * None of these error conditions are recoverable by the user, so we just dump |
391 | | - * an Internal Error on them. |
392 | | - */ |
393 | | - function swift_delete( $container, $rel ) { |
394 | | - try { |
395 | | - $container->delete_object( $rel ); |
396 | | - } catch ( SyntaxException $e ) { |
397 | | - throw new MWException( "Swift object name not well-formed: '$e'" ); |
398 | | - } catch ( NoSuchObjectException $e ) { |
399 | | - throw new MWException( "Swift object we are trying to delete does not exist: '$e'" ); |
400 | | - # } catch (InvalidResponseException $e) { |
401 | | - # throw new MWException( "unexpected response '$e'" ); |
402 | | - } |
403 | | - } |
404 | | - |
405 | | - /** |
406 | | - * Store a batch of files |
407 | | - * |
408 | | - * @param $triplets Array: (src,zone,dest) triplets as per store() |
409 | | - * @param $flags Integer: bitwise combination of the following flags: |
410 | | - * self::DELETE_SOURCE Delete the source file after upload |
411 | | - * self::OVERWRITE Overwrite an existing destination file instead of failing |
412 | | - * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the |
413 | | - * same contents as the source |
414 | | - * @return $status |
415 | | - */ |
416 | | - function storeBatch( $triplets, $flags = 0 ) { |
417 | | - wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) . |
418 | | - " triplets; flags: {$flags}\n" ); |
419 | | - |
420 | | - $status = $this->newGood(); |
421 | | - |
422 | | - // Execute the store operation for each triplet |
423 | | - $conn = $this->connect(); |
424 | | - |
425 | | - foreach ( $triplets as $i => $triplet ) { |
426 | | - list( $srcPath, $dstZone, $dstRel ) = $triplet; |
427 | | - |
428 | | - wfDebug( __METHOD__ . ": Storing $srcPath into $dstZone::$dstRel\n" ); |
429 | | - |
430 | | - // Point to the container. |
431 | | - $dstContainer = $this->getZoneContainer( $dstZone ); |
432 | | - $dstc = $this->get_container( $conn, $dstContainer ); |
433 | | - |
434 | | - $good = true; |
435 | | - |
436 | | - // Where are we copying this from? |
437 | | - if ( self::isVirtualUrl( $srcPath ) ) { |
438 | | - $src = $this->getContainerRel( $srcPath ); |
439 | | - list ( $srcContainer, $srcRel ) = $src; |
440 | | - $srcc = $this->get_container( $conn, $srcContainer ); |
441 | | - |
442 | | - // See if we're not supposed to overwrite an existing file. |
443 | | - if ( !( $flags & self::OVERWRITE ) ) { |
444 | | - // does it exist? |
445 | | - try { |
446 | | - $objd = $dstc->get_object( $dstRel ); |
447 | | - // and if it does, are we allowed to overwrite it? |
448 | | - if ( $flags & self::OVERWRITE_SAME ) { |
449 | | - $objs = $srcc->get_object( $srcRel ); |
450 | | - if ( $objd->getETag() != $objs->getETag() ) { |
451 | | - $status->fatal( 'fileexistserror', $dstRel ); |
452 | | - $good = false; |
453 | | - } |
454 | | - } else { |
455 | | - $status->fatal( 'fileexistserror', $dstRel ); |
456 | | - $good = false; |
457 | | - } |
458 | | - $exists = true; |
459 | | - } catch ( NoSuchObjectException $e ) { |
460 | | - $exists = false; |
461 | | - } |
462 | | - } |
463 | | - |
464 | | - if ( $good ) { |
465 | | - try { |
466 | | - $this->swiftcopy( $srcc, $srcRel, $dstc, $dstRel ); |
467 | | - } catch ( InvalidResponseException $e ) { |
468 | | - $status->error( 'filecopyerror', $srcPath, "{$dstc->name}/$dstRel" ); |
469 | | - $good = false; |
470 | | - } |
471 | | - if ( $flags & self::DELETE_SOURCE ) { |
472 | | - $this->swift_delete( $srcc, $srcRel ); |
473 | | - } |
474 | | - } |
475 | | - } else { |
476 | | - // See if we're not supposed to overwrite an existing file. |
477 | | - if ( !( $flags & self::OVERWRITE ) ) { |
478 | | - // does it exist? |
479 | | - try { |
480 | | - $objd = $dstc->get_object( $dstRel ); |
481 | | - // and if it does, are we allowed to overwrite it? |
482 | | - if ( $flags & self::OVERWRITE_SAME ) { |
483 | | - if ( $objd->getETag() != md5_file( $srcPath ) ) { |
484 | | - $status->fatal( 'fileexistserror', $dstRel ); |
485 | | - $good = false; |
486 | | - } |
487 | | - } else { |
488 | | - $status->fatal( 'fileexistserror', $dstRel ); |
489 | | - $good = false; |
490 | | - } |
491 | | - $exists = true; |
492 | | - } catch ( NoSuchObjectException $e ) { |
493 | | - $exists = false; |
494 | | - } |
495 | | - } |
496 | | - if ( $good ) { |
497 | | - wfDebug( __METHOD__ . ": Writing $srcPath to {$dstc->name}/$dstRel\n" ); |
498 | | - try { |
499 | | - $this->write_swift_object( $srcPath, $dstc, $dstRel ); |
500 | | - } catch ( InvalidResponseException $e ) { |
501 | | - $status->error( 'filecopyerror', $srcPath, "{$dstc->name}/$dstRel" ); |
502 | | - $good = false; |
503 | | - } |
504 | | - if ( $flags & self::DELETE_SOURCE ) { |
505 | | - unlink ( $srcPath ); |
506 | | - } |
507 | | - } |
508 | | - } |
509 | | - if ( $good ) { |
510 | | - $status->successCount++; |
511 | | - } else { |
512 | | - $status->failCount++; |
513 | | - } |
514 | | - $status->success[$i] = $good; |
515 | | - } |
516 | | - return $status; |
517 | | - } |
518 | | - |
519 | | - /** |
520 | | - * Append the contents of the source path to the given file, OR queue |
521 | | - * the appending operation in anticipation of a later appendFinish() call. |
522 | | - * @param $srcPath String: location of the source file |
523 | | - * @param $toAppendPath String: path to append to. |
524 | | - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate |
525 | | - * that the source file should be deleted if possible |
526 | | - * @return mixed Status or false |
527 | | - */ |
528 | | - |
529 | | - function append( $srcPath, $toAppendPath, $flags = 0 ) { |
530 | | - // Count the number of files whose names start with $toAppendPath |
531 | | - $conn = $this->connect(); |
532 | | - $container = $this->repo->get_container( $conn, $this->repo->container . "-temp" ); |
533 | | - $nextone = count( $container->list_objects( 0, NULL, $srcPath ) ); |
534 | | - |
535 | | - // Do the append to the next name |
536 | | - $status = $this->store( $srcPath, 'temp', sprintf( "%s.%05d", $toAppendPath, $nextone ) ); |
537 | | - |
538 | | - if ( $flags & self::DELETE_SOURCE ) { |
539 | | - unlink( $srcPath ); |
540 | | - } |
541 | | - |
542 | | - return $status; |
543 | | - } |
544 | | - /** |
545 | | - * Finish the append operation. |
546 | | - * @param $toAppendPath String: path to append to. |
547 | | - */ |
548 | | - function appendFinish( $toAppendPath ) { |
549 | | - $conn = $this->connect(); |
550 | | - $container = $this->repo->get_container( $conn, $this->repo->container . '-temp' ); |
551 | | - $parts = $container->list_objects( 0, NULL, $toAppendPath ); |
552 | | - // list_objects() returns a sorted list. |
553 | | - |
554 | | - // FIXME probably want to put this into a different container. |
555 | | - $biggie = $container->create_object( $toAppendPath ); |
556 | | - foreach ( $parts as $part ) { |
557 | | - $obj = $container->get_object( $part ); |
558 | | - $biggie->write( $obj->read() ); |
559 | | - $obj = $container->delete_object( $part ); |
560 | | - } |
561 | | - return Status::newGood(); |
562 | | - } |
563 | | - |
564 | | - /** |
565 | | - * Move a group of files to the deletion archive. |
566 | | - * If no valid deletion archive is configured, this may either delete the |
567 | | - * file or throw an exception, depending on the preference of the repository. |
568 | | - * |
569 | | - * @param $sourceDestPairs Array of source/destination pairs. Each element |
570 | | - * is a two-element array containing the source file path relative to the |
571 | | - * public root in the first element, and the archive file path relative |
572 | | - * to the deleted zone root in the second element. |
573 | | - * @return FileRepoStatus |
574 | | - */ |
575 | | - function deleteBatch( $sourceDestPairs ) { |
576 | | - wfDebug( __METHOD__ . ' deleting ' . var_export( $sourceDestPairs, true ) . '\n' ); |
577 | | - |
578 | | - /** |
579 | | - * Move the files |
580 | | - */ |
581 | | - $triplets = array(); |
582 | | - foreach ( $sourceDestPairs as $pair ) { |
583 | | - list( $srcRel, $archiveRel ) = $pair; |
584 | | - |
585 | | - $triplets[] = array( "mwrepo://{$this->name}/public/$srcRel", 'deleted', $archiveRel ); |
586 | | - |
587 | | - } |
588 | | - $status = $this->storeBatch( $triplets, FileRepo::OVERWRITE_SAME | FileRepo::DELETE_SOURCE ); |
589 | | - return $status; |
590 | | - } |
591 | | - |
592 | | - |
593 | | - function newFromArchiveName( $title, $archiveName ) { |
594 | | - return OldSwiftFile::newFromArchiveName( $title, $this, $archiveName ); |
595 | | - } |
596 | | - |
597 | | - /** |
598 | | - * Checks existence of specified array of files. |
599 | | - * |
600 | | - * @param $files Array: URLs of files to check |
601 | | - * @param $flags Integer: bitwise combination of the following flags: |
602 | | - * self::FILES_ONLY Mark file as existing only if it is a file (not directory) |
603 | | - * @return Either array of files and existence flags, or false |
604 | | - */ |
605 | | - function fileExistsBatch( $files, $flags = 0 ) { |
606 | | - if ( $flags != self::FILES_ONLY ) { |
607 | | - // we ONLY support when $flags & self::FILES_ONLY is set! |
608 | | - throw new MWException( "Swift Media Store doesn't have directories" ); |
609 | | - } |
610 | | - $result = array(); |
611 | | - $conn = $this->connect(); |
612 | | - |
613 | | - foreach ( $files as $key => $file ) { |
614 | | - if ( !self::isVirtualUrl( $file ) ) { |
615 | | - throw new MWException( __METHOD__ . " requires a virtual URL, not '$file'" ); |
616 | | - } |
617 | | - $rvu = $this->getContainerRel( $file ); |
618 | | - list ( $cont, $rel ) = $rvu; |
619 | | - $container = $this->get_container( $conn, $cont ); |
620 | | - try { |
621 | | - $obj = $container->get_object( $rel ); |
622 | | - $result[$key] = true; |
623 | | - } catch ( NoSuchObjectException $e ) { |
624 | | - $result[$key] = false; |
625 | | - } |
626 | | - } |
627 | | - |
628 | | - return $result; |
629 | | - } |
630 | | - |
631 | | - // FIXME: do we really need to reject empty titles? |
632 | | - function newFile( $title, $time = false ) { |
633 | | - if ( empty( $title ) ) { |
634 | | - return null; |
635 | | - } |
636 | | - return parent::newFile( $title, $time ); |
637 | | - } |
638 | | - |
639 | | - /** |
640 | | - * Copy a file from one place to another place in the same container |
641 | | - * @param $srcContainer CF_Container |
642 | | - * @param $srcRel String: relative path to the source file. |
643 | | - * @param $dstContainer CF_Container |
644 | | - * @param $dstRel String: relative path to the destination. |
645 | | - */ |
646 | | - protected function swiftcopy( $srcContainer, $srcRel, $dstContainer, $dstRel ) { |
647 | | - // The destination must exist already. |
648 | | - $obj = $dstContainer->create_object( $dstRel ); |
649 | | - $obj->content_type = 'text/plain'; |
650 | | - |
651 | | - try { |
652 | | - $obj->write( '.' ); |
653 | | - } catch ( SyntaxException $e ) { |
654 | | - throw new MWException( "Write failed: $e" ); |
655 | | - } catch ( BadContentTypeException $e ) { |
656 | | - throw new MWException( "Missing Content-Type: $e" ); |
657 | | - } catch ( MisMatchedChecksumException $e ) { |
658 | | - throw new MWException( __METHOD__ . "should not happen: '$e'" ); |
659 | | - } |
660 | | - |
661 | | - try { |
662 | | - $obj = $dstContainer->get_object( $dstRel ); |
663 | | - } catch ( NoSuchObjectException $e ) { |
664 | | - throw new MWException( 'The object we just created does not exist: ' . $dstContainer->name . "/$dstRel: $e" ); |
665 | | - } |
666 | | - |
667 | | - try { |
668 | | - $srcObj = $srcContainer->get_object( $srcRel ); |
669 | | - } catch ( NoSuchObjectException $e ) { |
670 | | - throw new MWException( 'Source file does not exist: ' . $srcContainer->name . "/$srcRel: $e" ); |
671 | | - } |
672 | | - |
673 | | - wfDebug( __METHOD__ . ' copying to ' . $dstContainer->name . "/$dstRel from " . $srcContainer->name . "/$srcRel\n" ); |
674 | | - |
675 | | - try { |
676 | | - $dstContainer->copy_object_from( $srcObj, $srcContainer, $dstRel ); |
677 | | - } catch ( SyntaxException $e ) { |
678 | | - throw new MWException( 'Source file does not exist: ' . $srcContainer->name . "/$srcRel: $e" ); |
679 | | - } catch ( MisMatchedChecksumException $e ) { |
680 | | - throw new MWException( "Checksums do not match: $e" ); |
681 | | - } |
682 | | - } |
683 | | - |
684 | | - /** |
685 | | - * Publish a batch of files |
686 | | - * @param $triplets Array: (source,dest,archive) triplets as per publish() |
687 | | - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate |
688 | | - * that the source files should be deleted if possible |
689 | | - */ |
690 | | - function publishBatch( $triplets, $flags = 0 ) { |
691 | | - |
692 | | - # paranoia |
693 | | - $status = $this->newGood( array() ); |
694 | | - foreach ( $triplets as $triplet ) { |
695 | | - list( $srcPath, $dstRel, $archiveRel ) = $triplet; |
696 | | - |
697 | | - if ( !$this->validateFilename( $dstRel ) ) { |
698 | | - throw new MWException( "Validation error in $dstRel" ); |
699 | | - } |
700 | | - if ( !$this->validateFilename( $archiveRel ) ) { |
701 | | - throw new MWException( "Validation error in $archiveRel" ); |
702 | | - } |
703 | | - } |
704 | | - |
705 | | - if ( !$status->ok ) { |
706 | | - return $status; |
707 | | - } |
708 | | - |
709 | | - try { |
710 | | - $conn = $this->connect(); |
711 | | - $container = $this->get_container( $conn, $this->container ); |
712 | | - } catch ( InvalidResponseException $e ) { |
713 | | - $status->fatal( "Unexpected Swift response: '$e'" ); |
714 | | - } |
715 | | - |
716 | | - if ( !$status->ok ) { |
717 | | - return $status; |
718 | | - } |
719 | | - |
720 | | - foreach ( $triplets as $i => $triplet ) { |
721 | | - list( $srcPath, $dstRel, $archiveRel ) = $triplet; |
722 | | - |
723 | | - // Archive destination file if it exists |
724 | | - try { |
725 | | - $pic = $container->get_object( $dstRel ); |
726 | | - } catch ( InvalidResponseException $e ) { |
727 | | - $status->error( "Unexpected Swift response: '$e'" ); |
728 | | - $status->failCount++; |
729 | | - continue; |
730 | | - } catch ( NoSuchObjectException $e ) { |
731 | | - $pic = NULL; |
732 | | - } |
733 | | - |
734 | | - if ( $pic ) { |
735 | | - $this->swiftcopy( $container, $dstRel, $container, $archiveRel ); |
736 | | - wfDebug( __METHOD__ . ": moved file $dstRel to $archiveRel\n" ); |
737 | | - $status->value[$i] = 'archived'; |
738 | | - } else { |
739 | | - $status->value[$i] = 'new'; |
740 | | - } |
741 | | - |
742 | | - $good = true; |
743 | | - try { |
744 | | - // Where are we copying this from? |
745 | | - if ( self::isVirtualUrl( $srcPath ) ) { |
746 | | - $src = $this->getContainerRel( $srcPath ); |
747 | | - list ( $srcContainer, $srcRel ) = $src; |
748 | | - $srcc = $this->get_container( $conn, $srcContainer ); |
749 | | - |
750 | | - $this->swiftcopy( $srcc, $srcRel, $container, $dstRel ); |
751 | | - if ( $flags & self::DELETE_SOURCE ) { |
752 | | - $this->swift_delete( $srcc, $srcRel ); |
753 | | - } |
754 | | - } else { |
755 | | - $this->write_swift_object( $srcPath, $container, $dstRel ); |
756 | | - // php-cloudfiles throws exceptions, so failure never gets here. |
757 | | - if ( $flags & self::DELETE_SOURCE ) { |
758 | | - unlink ( $srcPath ); |
759 | | - } |
760 | | - } |
761 | | - } catch ( InvalidResponseException $e ) { |
762 | | - $status->error( "Unexpected Swift response: '$e'" ); |
763 | | - $good = false; |
764 | | - } |
765 | | - |
766 | | - if ( $good ) { |
767 | | - $status->successCount++; |
768 | | - wfDebug( __METHOD__ . ": wrote tempfile $srcPath to $dstRel\n" ); |
769 | | - } else { |
770 | | - $status->failCount++; |
771 | | - } |
772 | | - } |
773 | | - return $status; |
774 | | - } |
775 | | - |
776 | | - /** |
777 | | - * Deletes a batch of files. Each file can be a (zone, rel) pairs, a |
778 | | - * virtual url or a real path. It will try to delete each file, but |
779 | | - * ignores any errors that may occur |
780 | | - * |
781 | | - * @param $pairs array List of files to delete |
782 | | - */ |
783 | | - function cleanupBatch( $files ) { |
784 | | - $conn = $this->connect(); |
785 | | - foreach ( $files as $file ) { |
786 | | - if ( is_array( $file ) ) { |
787 | | - // This is a pair, extract it |
788 | | - list( $cont, $rel ) = $file; |
789 | | - } else { |
790 | | - if ( self::isVirtualUrl( $file ) ) { |
791 | | - // This is a virtual url, resolve it |
792 | | - $path = $this->getContainerRel( $file ); |
793 | | - list( $cont, $rel ) = $path; |
794 | | - } else { |
795 | | - // FIXME: This is a full file name |
796 | | - throw new MWException( __METHOD__ . ": $file needs an unlink()" ); |
797 | | - } |
798 | | - } |
799 | | - |
800 | | - wfDebug( __METHOD__ . ": $cont/$rel\n" ); |
801 | | - $container = $this->get_container( $conn, $cont ); |
802 | | - $this->swift_delete( $container, $rel ); |
803 | | - } |
804 | | - } |
805 | | - |
806 | | - /** |
807 | | - * Delete files in the deleted directory if they are not referenced in the |
808 | | - * filearchive table. This needs to be done in the repo because it needs to |
809 | | - * interleave database locks with file operations, which is potentially a |
810 | | - * remote operation. |
811 | | - * @return FileRepoStatus |
812 | | - */ |
813 | | - function cleanupDeletedBatch( $storageKeys ) { |
814 | | - $conn = $this->connect(); |
815 | | - $cont = $this->getZoneContainer( 'deleted' ); |
816 | | - $container = $this->get_container( $conn, $cont ); |
817 | | - |
818 | | - $dbw = $this->getMasterDB(); |
819 | | - $status = $this->newGood(); |
820 | | - $storageKeys = array_unique( $storageKeys ); |
821 | | - foreach ( $storageKeys as $key ) { |
822 | | - $hashPath = $this->getDeletedHashPath( $key ); |
823 | | - $rel = "$hashPath$key"; |
824 | | - $dbw->begin(); |
825 | | - $inuse = $dbw->selectField( 'filearchive', '1', |
826 | | - array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), |
827 | | - __METHOD__, array( 'FOR UPDATE' ) ); |
828 | | - if ( !$inuse ) { |
829 | | - $sha1 = self::getHashFromKey( $key ); |
830 | | - $ext = substr( $key, strcspn( $key, '.' ) + 1 ); |
831 | | - $ext = File::normalizeExtension( $ext ); |
832 | | - $inuse = $dbw->selectField( 'oldimage', '1', |
833 | | - array( 'oi_sha1' => $sha1, |
834 | | - 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ), |
835 | | - $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ), |
836 | | - __METHOD__, array( 'FOR UPDATE' ) ); |
837 | | - } |
838 | | - if ( !$inuse ) { |
839 | | - wfDebug( __METHOD__ . ": deleting $key\n" ); |
840 | | - $this->swift_delete( $container, $rel ); |
841 | | - } else { |
842 | | - wfDebug( __METHOD__ . ": $key still in use\n" ); |
843 | | - $status->successCount++; |
844 | | - } |
845 | | - $dbw->commit(); |
846 | | - } |
847 | | - return $status; |
848 | | - } |
849 | | - |
850 | | - /** |
851 | | - * Makes no sense in our context -- don't let anybody call it. |
852 | | - */ |
853 | | - function getZonePath( $zone ) { |
854 | | - throw new MWException( __METHOD__ . ': not implemented' ); |
855 | | - } |
856 | | - |
857 | | - /** |
858 | | - * Get the Swift container corresponding to one of the three basic zones |
859 | | - */ |
860 | | - public function getZoneContainer( $zone ) { |
861 | | - switch ( $zone ) { |
862 | | - case 'public': |
863 | | - return $this->container; |
864 | | - case 'temp': |
865 | | - return $this->container . '-temp'; |
866 | | - case 'deleted': |
867 | | - return $this->container . '-deleted'; |
868 | | - case 'thumb': |
869 | | - return $this->container . '-thumb'; |
870 | | - default: |
871 | | - return false; |
872 | | - } |
873 | | - } |
874 | | - |
875 | | - /** |
876 | | - * Get a local path corresponding to a virtual URL |
877 | | - */ |
878 | | - protected function getContainerRel( $url ) { |
879 | | - if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { |
880 | | - throw new MWException( __METHOD__ . ': unknown protocol' ); |
881 | | - } |
882 | | - |
883 | | - $bits = explode( '/', substr( $url, 9 ), 3 ); |
884 | | - if ( count( $bits ) != 3 ) { |
885 | | - throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" ); |
886 | | - } |
887 | | - list( $repo, $zone, $rel ) = $bits; |
888 | | - if ( $repo !== $this->name ) { |
889 | | - throw new MWException( __METHOD__ . ': fetching from a foreign repo is not supported' ); |
890 | | - } |
891 | | - $container = $this->getZoneContainer( $zone ); |
892 | | - if ( $container === false ) { |
893 | | - throw new MWException( __METHOD__ . ": invalid zone: $zone" ); |
894 | | - } |
895 | | - return array( $container, rawurldecode( $rel ) ); |
896 | | - } |
897 | | - |
898 | | - /** |
899 | | - * Remove a temporary file or mark it for garbage collection |
900 | | - * @param $virtualUrl String: the virtual URL returned by storeTemp |
901 | | - * @return Boolean: true on success, false on failure |
902 | | - */ |
903 | | - function freeTemp( $virtualUrl ) { |
904 | | - $temp = "mwrepo://{$this->name}/temp"; |
905 | | - if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { |
906 | | - wfDebug( __METHOD__ . ": Invalid virtual URL\n" ); |
907 | | - return false; |
908 | | - } |
909 | | - $path = $this->getContainerRel( $virtualUrl ); |
910 | | - list ( $c, $r ) = $path; |
911 | | - $conn = $this->connect(); |
912 | | - $container = $this->get_container( $conn, $c ); |
913 | | - $this->swift_delete( $container, $r ); |
914 | | - } |
915 | | - |
916 | | - /** |
917 | | - * Called from elsewhere to turn a virtual URL into a path. |
918 | | - * Make sure you delete this file after you've used it!! |
919 | | - */ |
920 | | - function resolveVirtualUrl( $url ) { |
921 | | - $path = $this->getContainerRel( $url ); |
922 | | - list( $c, $r ) = $path; |
923 | | - return $this->getLocalCopy( $c, $r ); |
924 | | - } |
925 | | - |
926 | | - |
927 | | - /** |
928 | | - * Given a container and relative path, return an absolute path pointing at a |
929 | | - * copy of the file MUST delete the produced file, or else store it in |
930 | | - * SwiftFile->tempPath so it will be deleted when the object goes out of |
931 | | - * scope. |
932 | | - */ |
933 | | - function getLocalCopy( $container, $rel, $prefix = 'swift_in_' ) { |
934 | | - |
935 | | - // get a temporary place to put the original. |
936 | | - $tempPath = tempnam( wfTempDir(), $prefix ); |
937 | | - unlink( $tempPath ); |
938 | | - $tempPath .= '.' . pathinfo( $rel, PATHINFO_EXTENSION ); |
939 | | - |
940 | | - /* Fetch the image out of Swift */ |
941 | | - $conn = $this->connect(); |
942 | | - $cont = $this->get_container( $conn, $container ); |
943 | | - |
944 | | - try { |
945 | | - $obj = $cont->get_object( $rel ); |
946 | | - } catch ( NoSuchObjectException $e ) { |
947 | | - throw new MWException( "Unable to open original file at $container/$rel" ); |
948 | | - } |
949 | | - |
950 | | - wfDebug( __METHOD__ . " writing to $tempPath\n" ); |
951 | | - try { |
952 | | - $obj->save_to_filename( $tempPath ); |
953 | | - } catch ( IOException $e ) { |
954 | | - throw new MWException( __METHOD__ . ": error opening '$e'" ); |
955 | | - } catch ( InvalidResponseException $e ) { |
956 | | - throw new MWException( __METHOD__ . "unexpected response '$e'" ); |
957 | | - } |
958 | | - |
959 | | - return $tempPath; |
960 | | - } |
961 | | - |
962 | | - |
963 | | - /** |
964 | | - * Get properties of a file with a given virtual URL |
965 | | - * The virtual URL must refer to this repo |
966 | | - */ |
967 | | - function getFileProps( $virtualUrl ) { |
968 | | - $path = $this->resolveVirtualUrl( $virtualUrl ); |
969 | | - $ret = File::getPropsFromPath( $path ); |
970 | | - unlink( $path ); |
971 | | - return $ret; |
972 | | - } |
973 | | - |
974 | | - |
975 | | - /** |
976 | | - * Get an UploadStash associated with this repo. |
977 | | - * |
978 | | - * @return UploadStash |
979 | | - */ |
980 | | - function getUploadStash() { |
981 | | - return new SwiftStash( $this ); |
982 | | - } |
983 | | -} |
984 | | - |
985 | | -class SwiftStash extends UploadStash { |
986 | | - /** |
987 | | - * Wrapper function for subclassing. |
988 | | - */ |
989 | | - protected function newFile( $path, $key, $data ) { |
990 | | - wfDebug( __METHOD__ . ": deleting $key\n" ); |
991 | | - return new SwiftStashFile( $this, $this->repo, $path, $key, $data ); |
992 | | - } |
993 | | - |
994 | | -} |
995 | | - |
996 | | -class SwiftStashFile extends UploadStashFile { |
997 | | - // public function __construct( $stash, $repo, $path, $key, $data ) { |
998 | | - // // We don't call parent:: because UploadStashFile expects to be able to call $this->resolveURL() and get a pathname. |
999 | | - // $this->sessionStash = $stash; |
1000 | | - // $this->sessionKey = $key; |
1001 | | - // $this->sessionData = $data; |
1002 | | - // wfDebug( __METHOD__ . ": ($stash, $repo, $path, $key, $data)\n" ); |
1003 | | - |
1004 | | - // UnregisteredLocalFile::__construct( false, $repo, $path, false ); |
1005 | | - // $this->name = basename( $this->path ); |
1006 | | - |
1007 | | - // } |
1008 | | - |
1009 | | - // function getPath() { |
1010 | | - // } |
1011 | | -} |
1012 | | - |
1013 | | -/** |
1014 | | - * Old file in the in the oldimage table |
1015 | | - * |
1016 | | - * @file |
1017 | | - * @ingroup FileRepo |
1018 | | - */ |
1019 | | - |
1020 | | -/** |
1021 | | - * Class to represent a file in the oldimage table |
1022 | | - * |
1023 | | - * @ingroup FileRepo |
1024 | | - */ |
1025 | | -class OldSwiftFile extends SwiftFile { |
1026 | | - var $requestedTime, $archive_name; |
1027 | | - |
1028 | | - const CACHE_VERSION = 1; |
1029 | | - const MAX_CACHE_ROWS = 20; |
1030 | | - |
1031 | | - static function newFromTitle( $title, $repo, $time = null ) { |
1032 | | - # The null default value is only here to avoid an E_STRICT |
1033 | | - if ( $time === null ) |
1034 | | - throw new MWException( __METHOD__ . ' got null for $time parameter' ); |
1035 | | - return new self( $title, $repo, $time, null ); |
1036 | | - } |
1037 | | - |
1038 | | - static function newFromArchiveName( $title, $repo, $archiveName ) { |
1039 | | - return new self( $title, $repo, null, $archiveName ); |
1040 | | - } |
1041 | | - |
1042 | | - static function newFromRow( $row, $repo ) { |
1043 | | - $title = Title::makeTitle( NS_FILE, $row->oi_name ); |
1044 | | - $file = new self( $title, $repo, null, $row->oi_archive_name ); |
1045 | | - $file->loadFromRow( $row, 'oi_' ); |
1046 | | - return $file; |
1047 | | - } |
1048 | | - |
1049 | | - /** |
1050 | | - * @static |
1051 | | - * @param $sha1 |
1052 | | - * @param $repo LocalRepo |
1053 | | - * @param bool $timestamp |
1054 | | - * @return bool|OldLocalFile |
1055 | | - */ |
1056 | | - static function newFromKey( $sha1, $repo, $timestamp = false ) { |
1057 | | - $conds = array( 'oi_sha1' => $sha1 ); |
1058 | | - if ( $timestamp ) { |
1059 | | - $conds['oi_timestamp'] = $timestamp; |
1060 | | - } |
1061 | | - $dbr = $repo->getSlaveDB(); |
1062 | | - $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ ); |
1063 | | - if ( $row ) { |
1064 | | - return self::newFromRow( $row, $repo ); |
1065 | | - } else { |
1066 | | - return false; |
1067 | | - } |
1068 | | - } |
1069 | | - |
1070 | | - /** |
1071 | | - * Fields in the oldimage table |
1072 | | - */ |
1073 | | - static function selectFields() { |
1074 | | - return array( |
1075 | | - 'oi_name', |
1076 | | - 'oi_archive_name', |
1077 | | - 'oi_size', |
1078 | | - 'oi_width', |
1079 | | - 'oi_height', |
1080 | | - 'oi_metadata', |
1081 | | - 'oi_bits', |
1082 | | - 'oi_media_type', |
1083 | | - 'oi_major_mime', |
1084 | | - 'oi_minor_mime', |
1085 | | - 'oi_description', |
1086 | | - 'oi_user', |
1087 | | - 'oi_user_text', |
1088 | | - 'oi_timestamp', |
1089 | | - 'oi_deleted', |
1090 | | - 'oi_sha1', |
1091 | | - ); |
1092 | | - } |
1093 | | - |
1094 | | - /** |
1095 | | - * @param $title Title |
1096 | | - * @param $repo FileRepo |
1097 | | - * @param $time String: timestamp or null to load by archive name |
1098 | | - * @param $archiveName String: archive name or null to load by timestamp |
1099 | | - */ |
1100 | | - function __construct( $title, $repo, $time, $archiveName ) { |
1101 | | - parent::__construct( $title, $repo ); |
1102 | | - $this->requestedTime = $time; |
1103 | | - $this->archive_name = $archiveName; |
1104 | | - if ( is_null( $time ) && is_null( $archiveName ) ) { |
1105 | | - throw new MWException( __METHOD__ . ': must specify at least one of $time or $archiveName' ); |
1106 | | - } |
1107 | | - } |
1108 | | - |
1109 | | - function getCacheKey() { |
1110 | | - return false; |
1111 | | - } |
1112 | | - |
1113 | | - function getArchiveName() { |
1114 | | - if ( !isset( $this->archive_name ) ) { |
1115 | | - $this->load(); |
1116 | | - } |
1117 | | - return $this->archive_name; |
1118 | | - } |
1119 | | - |
1120 | | - function isOld() { |
1121 | | - return true; |
1122 | | - } |
1123 | | - |
1124 | | - function isVisible() { |
1125 | | - return $this->exists() && !$this->isDeleted( File::DELETED_FILE ); |
1126 | | - } |
1127 | | - |
1128 | | - function loadFromDB() { |
1129 | | - wfProfileIn( __METHOD__ ); |
1130 | | - $this->dataLoaded = true; |
1131 | | - $dbr = $this->repo->getSlaveDB(); |
1132 | | - $conds = array( 'oi_name' => $this->getName() ); |
1133 | | - if ( is_null( $this->requestedTime ) ) { |
1134 | | - $conds['oi_archive_name'] = $this->archive_name; |
1135 | | - } else { |
1136 | | - $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) ); |
1137 | | - } |
1138 | | - $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), |
1139 | | - $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) ); |
1140 | | - if ( $row ) { |
1141 | | - $this->loadFromRow( $row, 'oi_' ); |
1142 | | - } else { |
1143 | | - $this->fileExists = false; |
1144 | | - } |
1145 | | - wfProfileOut( __METHOD__ ); |
1146 | | - } |
1147 | | - |
1148 | | - function getCacheFields( $prefix = 'img_' ) { |
1149 | | - $fields = parent::getCacheFields( $prefix ); |
1150 | | - $fields[] = $prefix . 'archive_name'; |
1151 | | - $fields[] = $prefix . 'deleted'; |
1152 | | - return $fields; |
1153 | | - } |
1154 | | - |
1155 | | - function getRel() { |
1156 | | - return 'archive/' . $this->getHashPath() . $this->getArchiveName(); |
1157 | | - } |
1158 | | - |
1159 | | - function getUrlRel() { |
1160 | | - return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() ); |
1161 | | - } |
1162 | | - |
1163 | | - function upgradeRow() { |
1164 | | - wfProfileIn( __METHOD__ ); |
1165 | | - $this->loadFromFile(); |
1166 | | - |
1167 | | - # Don't destroy file info of missing files |
1168 | | - if ( !$this->fileExists ) { |
1169 | | - wfDebug( __METHOD__ . ': file does not exist, aborting\n' ); |
1170 | | - wfProfileOut( __METHOD__ ); |
1171 | | - return; |
1172 | | - } |
1173 | | - |
1174 | | - $dbw = $this->repo->getMasterDB(); |
1175 | | - list( $major, $minor ) = self::splitMime( $this->mime ); |
1176 | | - |
1177 | | - wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . ' to the current schema\n' ); |
1178 | | - $dbw->update( 'oldimage', |
1179 | | - array( |
1180 | | - 'oi_width' => $this->width, |
1181 | | - 'oi_height' => $this->height, |
1182 | | - 'oi_bits' => $this->bits, |
1183 | | - 'oi_media_type' => $this->media_type, |
1184 | | - 'oi_major_mime' => $major, |
1185 | | - 'oi_minor_mime' => $minor, |
1186 | | - 'oi_metadata' => $this->metadata, |
1187 | | - 'oi_sha1' => $this->sha1, |
1188 | | - ), array( |
1189 | | - 'oi_name' => $this->getName(), |
1190 | | - 'oi_archive_name' => $this->archive_name ), |
1191 | | - __METHOD__ |
1192 | | - ); |
1193 | | - wfProfileOut( __METHOD__ ); |
1194 | | - } |
1195 | | - |
1196 | | - /** |
1197 | | - * @param $field Integer: one of DELETED_* bitfield constants |
1198 | | - * for file or revision rows |
1199 | | - * @return bool |
1200 | | - */ |
1201 | | - function isDeleted( $field ) { |
1202 | | - $this->load(); |
1203 | | - return ( $this->deleted & $field ) == $field; |
1204 | | - } |
1205 | | - |
1206 | | - /** |
1207 | | - * Returns bitfield value |
1208 | | - * @return int |
1209 | | - */ |
1210 | | - function getVisibility() { |
1211 | | - $this->load(); |
1212 | | - return (int)$this->deleted; |
1213 | | - } |
1214 | | - |
1215 | | - /** |
1216 | | - * Determine if the current user is allowed to view a particular |
1217 | | - * field of this image file, if it's marked as deleted. |
1218 | | - * |
1219 | | - * @param $field Integer |
1220 | | - * @return bool |
1221 | | - */ |
1222 | | - function userCan( $field ) { |
1223 | | - $this->load(); |
1224 | | - return Revision::userCanBitfield( $this->deleted, $field ); |
1225 | | - } |
1226 | | -} |
1227 | | - |
1228 | | -/** |
1229 | | - * Foreign file with an accessible MediaWiki database |
1230 | | - * |
1231 | | - * @ingroup FileRepo |
1232 | | - */ |
1233 | | -class SwiftForeignDBFile extends SwiftFile { |
1234 | | - |
1235 | | - /** |
1236 | | - * @param $title |
1237 | | - * @param $repo |
1238 | | - * @param $unused |
1239 | | - * @return SwiftForeignDBFile |
1240 | | - */ |
1241 | | - static function newFromTitle( $title, $repo, $unused = null ) { |
1242 | | - return new self( $title, $repo ); |
1243 | | - } |
1244 | | - |
1245 | | - /** |
1246 | | - * Create a ForeignDBFile from a title |
1247 | | - * Do not call this except from inside a repo class. |
1248 | | - */ |
1249 | | - static function newFromRow( $row, $repo ) { |
1250 | | - $title = Title::makeTitle( NS_FILE, $row->img_name ); |
1251 | | - $file = new self( $title, $repo ); |
1252 | | - $file->loadFromRow( $row ); |
1253 | | - return $file; |
1254 | | - } |
1255 | | - |
1256 | | - function publish( $srcPath, $flags = 0 ) { |
1257 | | - $this->readOnlyError(); |
1258 | | - } |
1259 | | - |
1260 | | - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', |
1261 | | - $watch = false, $timestamp = false ) { |
1262 | | - $this->readOnlyError(); |
1263 | | - } |
1264 | | - |
1265 | | - function restore( $versions = array(), $unsuppress = false ) { |
1266 | | - $this->readOnlyError(); |
1267 | | - } |
1268 | | - |
1269 | | - function delete( $reason, $suppress = false ) { |
1270 | | - $this->readOnlyError(); |
1271 | | - } |
1272 | | - |
1273 | | - function move( $target ) { |
1274 | | - $this->readOnlyError(); |
1275 | | - } |
1276 | | - |
1277 | | - function getDescriptionUrl() { |
1278 | | - // Restore remote behaviour |
1279 | | - return File::getDescriptionUrl(); |
1280 | | - } |
1281 | | - |
1282 | | - function getDescriptionText() { |
1283 | | - // Restore remote behaviour |
1284 | | - return File::getDescriptionText(); |
1285 | | - } |
1286 | | -} |
1287 | | - |
1288 | | -/** |
1289 | | - * A foreign repository with an accessible MediaWiki database |
1290 | | - * |
1291 | | - * @ingroup FileRepo |
1292 | | - */ |
1293 | | -class SwiftForeignDBRepo extends SwiftRepo { |
1294 | | - # Settings |
1295 | | - var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags, |
1296 | | - $tablePrefix, $hasSharedCache; |
1297 | | - |
1298 | | - # Other stuff |
1299 | | - var $dbConn; |
1300 | | - var $fileFactory = array( 'SwiftForeignDBFile', 'newFromTitle' ); |
1301 | | - var $fileFromRowFactory = array( 'SwiftForeignDBFile', 'newFromRow' ); |
1302 | | - |
1303 | | - function __construct( $info ) { |
1304 | | - parent::__construct( $info ); |
1305 | | - $this->dbType = $info['dbType']; |
1306 | | - $this->dbServer = $info['dbServer']; |
1307 | | - $this->dbUser = $info['dbUser']; |
1308 | | - $this->dbPassword = $info['dbPassword']; |
1309 | | - $this->dbName = $info['dbName']; |
1310 | | - $this->dbFlags = $info['dbFlags']; |
1311 | | - $this->tablePrefix = $info['tablePrefix']; |
1312 | | - $this->hasSharedCache = $info['hasSharedCache']; |
1313 | | - } |
1314 | | - |
1315 | | - /** |
1316 | | - * @return DatabaseBase |
1317 | | - */ |
1318 | | - function getMasterDB() { |
1319 | | - wfDebug( __METHOD__ . ": {$this->dbServer}\n" ); |
1320 | | - if ( !isset( $this->dbConn ) ) { |
1321 | | - $this->dbConn = DatabaseBase::factory( $this->dbType, |
1322 | | - array( |
1323 | | - 'host' => $this->dbServer, |
1324 | | - 'user' => $this->dbUser, |
1325 | | - 'password' => $this->dbPassword, |
1326 | | - 'dbname' => $this->dbName, |
1327 | | - 'flags' => $this->dbFlags, |
1328 | | - 'tablePrefix' => $this->tablePrefix |
1329 | | - ) |
1330 | | - ); |
1331 | | - } |
1332 | | - return $this->dbConn; |
1333 | | - } |
1334 | | - |
1335 | | - /** |
1336 | | - * @return DatabaseBase |
1337 | | - */ |
1338 | | - function getSlaveDB() { |
1339 | | - return $this->getMasterDB(); |
1340 | | - } |
1341 | | - |
1342 | | - function hasSharedCache() { |
1343 | | - return $this->hasSharedCache; |
1344 | | - } |
1345 | | - |
1346 | | - /** |
1347 | | - * Get a key on the primary cache for this repository. |
1348 | | - * Returns false if the repository's cache is not accessible at this site. |
1349 | | - * The parameters are the parts of the key, as for wfMemcKey(). |
1350 | | - */ |
1351 | | - function getSharedCacheKey( /*...*/ ) { |
1352 | | - if ( $this->hasSharedCache() ) { |
1353 | | - $args = func_get_args(); |
1354 | | - array_unshift( $args, $this->dbName, $this->tablePrefix ); |
1355 | | - return call_user_func_array( 'wfForeignMemcKey', $args ); |
1356 | | - } else { |
1357 | | - return false; |
1358 | | - } |
1359 | | - } |
1360 | | - |
1361 | | - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
1362 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1363 | | - } |
1364 | | - |
1365 | | - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
1366 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1367 | | - } |
1368 | | - |
1369 | | - function deleteBatch( $sourceDestPairs ) { |
1370 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1371 | | - } |
1372 | | -} |
1373 | | - |
1374 | | -/** |
1375 | | - * A foreign repository with a MediaWiki database accessible via the configured LBFactory |
1376 | | - * |
1377 | | - * @file |
1378 | | - * @ingroup FileRepo |
1379 | | - */ |
1380 | | - |
1381 | | -/** |
1382 | | - * A foreign repository with a MediaWiki database accessible via the configured LBFactory |
1383 | | - * |
1384 | | - * @ingroup FileRepo |
1385 | | - */ |
1386 | | -class SwiftForeignDBViaLBRepo extends SwiftRepo { |
1387 | | - var $wiki, $dbName, $tablePrefix; |
1388 | | - var $fileFactory = array( 'SwiftForeignDBFile', 'newFromTitle' ); |
1389 | | - var $fileFromRowFactory = array( 'SwiftForeignDBFile', 'newFromRow' ); |
1390 | | - |
1391 | | - function __construct( $info ) { |
1392 | | - parent::__construct( $info ); |
1393 | | - $this->wiki = $info['wiki']; |
1394 | | - list( $this->dbName, $this->tablePrefix ) = wfSplitWikiID( $this->wiki ); |
1395 | | - $this->hasSharedCache = $info['hasSharedCache']; |
1396 | | - } |
1397 | | - |
1398 | | - /** |
1399 | | - * @return DatabaseBase |
1400 | | - */ |
1401 | | - function getMasterDB() { |
1402 | | - return wfGetDB( DB_MASTER, array(), $this->wiki ); |
1403 | | - } |
1404 | | - |
1405 | | - /** |
1406 | | - * @return DatabaseBase |
1407 | | - */ |
1408 | | - function getSlaveDB() { |
1409 | | - return wfGetDB( DB_SLAVE, array(), $this->wiki ); |
1410 | | - } |
1411 | | - |
1412 | | - /** |
1413 | | - * @return bool |
1414 | | - */ |
1415 | | - function hasSharedCache() { |
1416 | | - return $this->hasSharedCache; |
1417 | | - } |
1418 | | - |
1419 | | - /** |
1420 | | - * Get a key on the primary cache for this repository. |
1421 | | - * Returns false if the repository's cache is not accessible at this site. |
1422 | | - * The parameters are the parts of the key, as for wfMemcKey(). |
1423 | | - */ |
1424 | | - function getSharedCacheKey( /*...*/ ) { |
1425 | | - if ( $this->hasSharedCache() ) { |
1426 | | - $args = func_get_args(); |
1427 | | - array_unshift( $args, $this->wiki ); |
1428 | | - return implode( ':', $args ); |
1429 | | - } else { |
1430 | | - return false; |
1431 | | - } |
1432 | | - } |
1433 | | - |
1434 | | - function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
1435 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1436 | | - } |
1437 | | - |
1438 | | - function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { |
1439 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1440 | | - } |
1441 | | - |
1442 | | - function deleteBatch( $fileMap ) { |
1443 | | - throw new MWException( get_class( $this ) . ': write operations are not supported' ); |
1444 | | - } |
1445 | | -} |
Index: trunk/extensions/SwiftMedia/wmf/tests/smtest.py |
— | — | @@ -0,0 +1,169 @@ |
| 2 | +#!/usr/bin/python |
| 3 | +# http://www.deheus.net/petrik/blog/2005/11/20/creating-a-wikipedia-watchlist-rss-feed-with-python-and-twill/ |
| 4 | + |
| 5 | +import sys, string, datetime, time, os, re, stat |
| 6 | +import twill |
| 7 | +import twill.commands as t |
| 8 | +import gd |
| 9 | + |
| 10 | +temp_html = "/tmp/wikipedia.html" |
| 11 | +rss_title = "Wikipedia watchlist" |
| 12 | +rss_link = "http://en.wikipedia.org" |
| 13 | +host = "http://ersch.wikimedia.org/" |
| 14 | +#host = "http://127.0.0.1/wiki/" |
| 15 | + |
| 16 | +def login(username, password): |
| 17 | + t.add_extra_header("User-Agent", "python-twill-russnelson@gmail.com") |
| 18 | + |
| 19 | + t.go(host+"index.php/Special:UserLogin") |
| 20 | + t.fv("1", "wpName", username) |
| 21 | + t.fv("1", "wpPassword", password) |
| 22 | + t.submit("wpLoginAttempt") |
| 23 | + |
| 24 | + |
| 25 | +def upload_list(browser, pagename, uploads): |
| 26 | + |
| 27 | + # get the file sizes for later comparison. |
| 28 | + filesizes = [] |
| 29 | + for fn in uploads: |
| 30 | + filesizes.append(os.stat(fn)[stat.ST_SIZE]) |
| 31 | + filesizes.reverse() # because they get listed newest first. |
| 32 | + |
| 33 | + # Upload copy #1. |
| 34 | + t.go(host+"index.php/Special:Upload") |
| 35 | + t.formfile("1", "wpUploadFile", uploads[0]) |
| 36 | + t.fv("1", "wpDestFile", pagename) |
| 37 | + t.fv("1", "wpUploadDescription", "Uploading %s" % pagename) |
| 38 | + t.submit("wpUpload") |
| 39 | + |
| 40 | + # Verify that we succeeded. |
| 41 | + t.find("File:%s" % pagename) |
| 42 | + |
| 43 | + for fn in uploads[1:]: |
| 44 | + # propose that we upload a replacement |
| 45 | + t.go(host+"index.php?title=Special:Upload&wpDestFile=%s&wpForReUpload=1" % pagename) |
| 46 | + t.formfile("1", "wpUploadFile", fn) |
| 47 | + t.fv("1", "wpUploadDescription", "Uploading %s as %s" % (fn, pagename)) |
| 48 | + t.submit("wpUpload") |
| 49 | + |
| 50 | + # get the URLs for the thumbnails |
| 51 | + urls = [] |
| 52 | + for url in re.finditer(r'<td><a href="([^"]*?)"><img alt="Thumbnail for version .*?" src="(.*?)"', browser.get_html()): |
| 53 | + urls.append(url.group(1)) |
| 54 | + urls.append(url.group(2)) |
| 55 | + |
| 56 | + print filesizes |
| 57 | + for i, url in enumerate(urls): |
| 58 | + t.go(url) |
| 59 | + if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]: |
| 60 | + print i,len(browser.get_html()), filesizes[i / 2] |
| 61 | + t.find("Files differ in size") |
| 62 | + t.code("200") |
| 63 | + t.back() |
| 64 | + |
| 65 | + # delete all versions |
| 66 | + t.go(host+"index.php?title=File:%s&action=delete" % pagename) |
| 67 | + # after we get the confirmation page, commit to the action. |
| 68 | + t.fv("1", "wpReason", "Test Deleting...") |
| 69 | + t.submit("mw-filedelete-submit") |
| 70 | + |
| 71 | + # make sure that we can't visit their URLs. |
| 72 | + for i, url in enumerate(urls): |
| 73 | + t.go(url) |
| 74 | + if 0 and i % 2 == 1 and i > 0 and browser.get_code() == 200: |
| 75 | + # bug 30192: the archived file's thumbnail doesn't get deleted. |
| 76 | + print "special-casing the last URL" |
| 77 | + continue |
| 78 | + t.code("404") |
| 79 | + |
| 80 | + # restore the current and archived version. |
| 81 | + t.go(host+"index.php/Special:Undelete/File:%s" % pagename) |
| 82 | + t.fv("1", "wpComment", "Test Restore") |
| 83 | + t.submit("restore") |
| 84 | + |
| 85 | + # visit the page to make sure that the thumbs get re-rendered properly. |
| 86 | + # when we get the 404 handler working correctly, this won't be needed. |
| 87 | + t.go(host+"index.php?title=File:%s" % pagename) |
| 88 | + |
| 89 | + # make sure that they got restored correctly. |
| 90 | + for i, url in enumerate(urls): |
| 91 | + t.go(url) |
| 92 | + if i % 2 == 0 and len(browser.get_html()) != filesizes[i / 2]: |
| 93 | + t.find("Files differ in size") |
| 94 | + t.code("200") |
| 95 | + t.back() |
| 96 | + |
| 97 | + if len(uploads) != 2: |
| 98 | + return |
| 99 | + |
| 100 | + match = re.search(r'"([^"]+?)" title="[^"]+?">revert', browser.get_html()) |
| 101 | + if not match: |
| 102 | + t.find('revert') |
| 103 | + t.go(match.group(1).replace('&', '&')) |
| 104 | + |
| 105 | +def make_files(pagename): |
| 106 | + redfilename = "/tmp/Red-%s" % pagename |
| 107 | + greenfilename = "/tmp/Green-%s" % pagename |
| 108 | + bluefilename = "/tmp/Blue-%s" % pagename |
| 109 | + |
| 110 | + # create a small test image. |
| 111 | + gd.gdMaxColors = 256 |
| 112 | + i = gd.image((200,100)) |
| 113 | + black = i.colorAllocate((0,0,0)) |
| 114 | + white = i.colorAllocate((255,255,255)) |
| 115 | + red = i.colorAllocate((255,55,55)) |
| 116 | + green = i.colorAllocate((55,255,55)) |
| 117 | + blue = i.colorAllocate((55,55,255)) |
| 118 | + |
| 119 | + # now write a red version |
| 120 | + i.rectangle((0,0),(199,99),red, red) |
| 121 | + i.line((0,0),(199,99),black) |
| 122 | + i.string(gd.gdFontLarge, (5,50), pagename, white) |
| 123 | + i.writePng(redfilename) |
| 124 | + |
| 125 | + # now write a green version |
| 126 | + i.rectangle((0,0),(199,99),green, green) |
| 127 | + i.line((0,0),(99,99),black) |
| 128 | + i.string(gd.gdFontLarge, (5,50), pagename, white) |
| 129 | + i.writePng(greenfilename) |
| 130 | + |
| 131 | + # write a blue version |
| 132 | + i.rectangle((0,0),(199,99),blue,blue) |
| 133 | + i.line((0,0),(99,199),black) |
| 134 | + i.string(gd.gdFontLarge, (5,50), pagename, white) |
| 135 | + i.writePng(bluefilename) |
| 136 | + |
| 137 | + # propose that we delete it (in case it exists) |
| 138 | + t.go(host+"index.php?title=File:%s&action=delete" % pagename) |
| 139 | + # make sure that we've NOT gotten the wrong page and HAVE gotten the right one. |
| 140 | + t.notfind('You are about to delete the file') |
| 141 | + t.find("could not be deleted") |
| 142 | + |
| 143 | + return (redfilename, greenfilename, bluefilename ) |
| 144 | + |
| 145 | +def main(): |
| 146 | + try: |
| 147 | + username = sys.argv[1] |
| 148 | + password = sys.argv[2] |
| 149 | + except IndexError: |
| 150 | + print "Please supply username password" |
| 151 | + sys.exit(1) |
| 152 | + browser = twill.get_browser() |
| 153 | + login(username, password) |
| 154 | + |
| 155 | + serial = time.time() |
| 156 | + pagename = "Test-%s.png" % serial |
| 157 | + filenames = make_files(pagename) |
| 158 | + upload_list(browser, pagename, filenames[0:2]) |
| 159 | + |
| 160 | + # try it again with two replacement files. |
| 161 | +# pagename = "Test-%sA.png" % serial |
| 162 | +# filenames = make_files(pagename) |
| 163 | +# upload_list(browser, pagename, filenames) |
| 164 | + |
| 165 | + t.showforms() |
| 166 | + t.save_html("/tmp/testabcd") |
| 167 | + |
| 168 | +if __name__ == "__main__": |
| 169 | + main() |
| 170 | + |
Property changes on: trunk/extensions/SwiftMedia/wmf/tests/smtest.py |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 171 | + native |
Added: svn:executable |
2 | 172 | + * |
Index: trunk/extensions/SwiftMedia/wmf/tests/test_rewrite.py |
— | — | @@ -0,0 +1,171 @@ |
| 2 | +#!/usr/bin/python |
| 3 | + |
| 4 | +import unittest |
| 5 | + |
| 6 | +import webob |
| 7 | + |
| 8 | +from wmf import rewrite |
| 9 | +from wmf.client import ClientException |
| 10 | + |
| 11 | +class FakeApp(object): |
| 12 | + def __init__(self, status, headers): |
| 13 | + self.status = status |
| 14 | + self.headers = headers |
| 15 | + |
| 16 | + def __call__(self, env, start_response): |
| 17 | + start_response(self.status, self.headers) |
| 18 | + return "FAKE APP" |
| 19 | + |
| 20 | +def start_response(*args): |
| 21 | + pass |
| 22 | + |
| 23 | +class TestRewrite(unittest.TestCase): |
| 24 | + |
| 25 | + def setUp(self): |
| 26 | + pass |
| 27 | + |
| 28 | + account="AUTH_..." |
| 29 | + urlbig = 'http://alsted.wikimedia.org/wikipedia/commons/a/aa/'\ |
| 30 | + 'Dzimbo_u_Beogradu_19.jpeg' |
| 31 | + urlorig = '/v1/' + account + \ |
| 32 | + '/wikipedia%2Fcommons/a/aa/Dzimbo_u_Beogradu_19.jpeg' |
| 33 | + thumbbig = 'http://alsted.wikimedia.org/wikipedia/commons/thumb/a/aa/'\ |
| 34 | + 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg' |
| 35 | + url448 = '/v1/' + account + \ |
| 36 | + '/wikipedia%2Fcommons%2Fthumb/a/aa/'\ |
| 37 | + 'Dzimbo_u_Beogradu_19.jpeg/448px-Dzimbo_u_Beogradu_19.jpeg' |
| 38 | + urlaccount = 'http://alsted.wikimedia.org/' + account |
| 39 | + contname = '/wikipedia/commons/thumb' |
| 40 | + objname = '/a/aa/Dzimbo_u_Beogradu_19.jpeg/91px-Dzimbo_u_Beogradu_19.jpeg' |
| 41 | + a = dict(account=account, |
| 42 | + url="https://127.0.0.1:11000/v1.0", |
| 43 | + login="yourlogin", |
| 44 | + thumbhost='localhost', |
| 45 | + user_agent='Mozilla/5.0', |
| 46 | + key="yourkey") |
| 47 | + |
| 48 | + def test_01(self): |
| 49 | + """#01 Cur controller can snarf its args.""" |
| 50 | + controller = rewrite.ObjectController() |
| 51 | + controller.do_start_response("200 Good", {"test": "testy"}) |
| 52 | + self.assertEquals(controller.response_args[0], "200 Good") |
| 53 | + self.assertEquals(controller.response_args[1], {"test": "testy"}) |
| 54 | + |
| 55 | + def test_01a(self): |
| 56 | + """#01a Our app calls into the FakeApp; returns its results if 200 """ |
| 57 | + app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
| 58 | + req = webob.Request.blank(self.urlbig, |
| 59 | + environ={'REQUEST_METHOD': 'GET'}) |
| 60 | + controller = rewrite.ObjectController() |
| 61 | + resp = app(req.environ, controller.do_start_response) |
| 62 | + self.assertEquals(resp, 'FAKE APP') |
| 63 | + |
| 64 | + def test_02(self): |
| 65 | + """#02 Test URL rewriting for originals. """ |
| 66 | + app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
| 67 | + req = webob.Request.blank(self.urlbig, |
| 68 | + environ={'REQUEST_METHOD': 'GET'}) |
| 69 | + resp = app(req.environ, start_response) |
| 70 | + self.assertEquals(req.environ['PATH_INFO'], self.urlorig) |
| 71 | + |
| 72 | + def test_03(self): |
| 73 | + """#03 Test URL rewriting for thumbs. """ |
| 74 | + app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
| 75 | + req = webob.Request.blank(self.thumbbig, |
| 76 | + environ={'REQUEST_METHOD': 'GET'}) |
| 77 | + resp = app(req.environ, start_response) |
| 78 | + self.assertEquals(req.environ['PATH_INFO'], self.url448) |
| 79 | + |
| 80 | + def test_04(self): |
| 81 | + """#04 Test a write. Could fail if our token has gone stale""" |
| 82 | + app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a) |
| 83 | + req = webob.Request.blank(self.thumbbig, |
| 84 | + environ={'REQUEST_METHOD': 'GET'}) |
| 85 | + resp = app(req.environ, start_response) |
| 86 | + self.assertEquals(req.environ['PATH_INFO'], self.url448) |
| 87 | + # note that we PUT this file onto the server here, even if it's already there. |
| 88 | + datalen = 0 |
| 89 | + for data in resp: |
| 90 | + datalen += len(data) |
| 91 | + self.assertEquals(datalen, 51543) |
| 92 | + |
| 93 | + def test_05(self): |
| 94 | + """#05 Report 401 (authorization) errors""" |
| 95 | + app = rewrite.WMFRewrite(FakeApp("401 Bad", {}),self.a) |
| 96 | + req = webob.Request.blank(self.thumbbig, |
| 97 | + environ={'REQUEST_METHOD': 'GET'}) |
| 98 | + resp = app(req.environ, start_response) |
| 99 | + self.assertEquals(req.environ['PATH_INFO'], self.url448) |
| 100 | + self.assertEquals(resp, |
| 101 | + ['401 Unauthorized\n\nThis server could not verify that you are '\ |
| 102 | + 'authorized to access the document you requested. Either you '\ |
| 103 | + 'supplied the wrong credentials (e.g., bad password), or your '\ |
| 104 | + 'browser does not understand how to supply the credentials '\ |
| 105 | + 'required.\n\n Token may have timed out ']) |
| 106 | + |
| 107 | + def test_06(self): |
| 108 | + """#06 Give them a bad token so that the PUT fails.""" |
| 109 | + app = rewrite.WMFRewrite(FakeApp("404 Bad", {}),self.a) |
| 110 | + app.token = "HaHaYeahRight" |
| 111 | + req = webob.Request.blank(self.thumbbig, |
| 112 | + environ={'REQUEST_METHOD': 'GET'}) |
| 113 | + resp = app(req.environ, start_response) |
| 114 | + self.assertEquals(req.environ['PATH_INFO'], self.url448) |
| 115 | + # the PUT fails because we give them a bad token, but ... we should |
| 116 | + # really silently just hand back the file. |
| 117 | + try: |
| 118 | + datalen = 0 |
| 119 | + for data in resp: |
| 120 | + datalen += len(data) |
| 121 | + except ClientException, x: |
| 122 | + self.assertEquals(datalen, 51543) |
| 123 | + y = "ClientException('Object PUT failed',)" |
| 124 | + self.assertEquals(`x`, y) |
| 125 | + |
| 126 | + def test_07(self): |
| 127 | + """#07 Make sure that an already-authorized path goes unchanged.""" |
| 128 | + app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
| 129 | + url = self.urlaccount + self.contname + self.objname |
| 130 | + req = webob.Request.blank(url, environ={'REQUEST_METHOD': 'GET'}) |
| 131 | + resp = app(req.environ, start_response) |
| 132 | + self.assertEquals(req.url, url) # should remain unchanged |
| 133 | + #self.assertTrue(len(app.response_args) == 0) # no args either. |
| 134 | + |
| 135 | + def test_08(self): |
| 136 | + """#08 Don't let them read the container""" |
| 137 | + app = rewrite.WMFRewrite(FakeApp("200 Good", {}),self.a) |
| 138 | + req = webob.Request.blank( |
| 139 | + 'http://alsted.wikimedia.org/wikipedia/commons/', |
| 140 | + environ={'REQUEST_METHOD': 'GET'}) |
| 141 | + resp = app(req.environ, start_response) |
| 142 | + self.assertEquals(resp, |
| 143 | + ['403 Forbidden\n\nAccess was denied to this resource.\n\n '\ |
| 144 | + 'No container listing ']) |
| 145 | + |
| 146 | + # test_09 became obsolete |
| 147 | + |
| 148 | + def test_10(self): |
| 149 | + """#10 Trap weird-ass errors""" |
| 150 | + app = rewrite.WMFRewrite(FakeApp("999 Unrecognized", {}),self.a) |
| 151 | + req = webob.Request.blank("http://localhost/a/b/c", |
| 152 | + environ={'REQUEST_METHOD': 'GET'}) |
| 153 | + resp = app(req.environ, start_response) |
| 154 | + self.assertEquals(resp, |
| 155 | + ['501 Not Implemented\n\nThe server has either erred or is '\ |
| 156 | + 'incapable of performing the requested operation.\n\n Unknown '\ |
| 157 | + 'Status: 999 ']) |
| 158 | + |
| 159 | + def test_11(self): |
| 160 | + """#11 Trap URLs that don't match the regexp""" |
| 161 | + app = rewrite.WMFRewrite(FakeApp("404 TryRegexp", {}),self.a) |
| 162 | + req = webob.Request.blank("http://localhost/a", |
| 163 | + environ={'REQUEST_METHOD': 'GET'}) |
| 164 | + resp = app(req.environ, start_response) |
| 165 | + self.assertEquals(resp, |
| 166 | + ['400 Bad Request\n\nThe server could not comply with the '\ |
| 167 | + 'request since it is either malformed or otherwise '\ |
| 168 | + 'incorrect.\n\n Regexp failed: "/a" ']) |
| 169 | + |
| 170 | +if __name__ == '__main__': |
| 171 | + unittest.main() |
| 172 | + |
Property changes on: trunk/extensions/SwiftMedia/wmf/tests/test_rewrite.py |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 173 | + native |
Added: svn:executable |
2 | 174 | + * |