summaryrefslogtreecommitdiff
path: root/src/librygel-server/rygel-http-post.vala
blob: d72ca5e3e89a51ea0428aaa2229c7ec12ea41d5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/*
 * Copyright (C) 2008-2010 Nokia Corporation.
 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
 *
 * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 *                               <zeeshan.ali@nokia.com>
 *         Jorn Baayen <jorn.baayen@gmail.com>
 *
 * This file is part of Rygel.
 *
 * Rygel is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * Rygel is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

using Soup;

/**
 * Responsible for handling HTTP POST client requests.
 */
internal class Rygel.HTTPPost : HTTPRequest {
    SourceFunc handle_continue;

    File file;
    File dotfile;
    OutputStream stream;

    public HTTPPost (HTTPServer   http_server,
                     Soup.Server  server,
                     Soup.ServerMessage msg) {
        base (http_server, server, msg);

        this.cancellable.connect (this.on_request_cancelled);
        msg.get_request_body ().set_accumulate (false);
    }

    protected override async void handle () throws Error {
        var queue = ObjectRemovalQueue.get_default ();
        queue.dequeue (this.object);

        try {
            yield this.handle_real ();
        } catch (Error error) {
            yield queue.remove_now (this.object, this.cancellable);

            throw error;
        }
    }

    private async void handle_real () throws Error {
        var item = (MediaFileItem) this.object;

        if (!item.place_holder) {
            var msg = _("Pushing data to non-empty item ā€œ%sā€ not allowed");

            throw new ContentDirectoryError.INVALID_ARGS (msg, this.object.id);
        }

        this.file = yield item.get_writable (this.cancellable);
        if (this.file == null) {
            throw new HTTPRequestError.BAD_REQUEST
                                        (_("No writable URI for %s available"),
                                         this.object.id);
        }

        this.dotfile = this.file.get_parent ().get_child
                                        ("." + this.file.get_basename ());
        this.stream = yield this.dotfile.replace_async
                                        (null,
                                         false,
                                         FileCreateFlags.REPLACE_DESTINATION,
                                         Priority.LOW,
                                         this.cancellable);

        this.msg.got_chunk.connect (this.on_got_chunk);
        this.msg.got_body.connect (this.on_got_body);

        this.server.unpause_message (this.msg);
        this.handle_continue = this.handle_real.callback;

        yield;
    }

    private void on_got_body (ServerMessage msg) {
        if (this.msg != msg) {
            return;
        }

        this.finalize_post.begin ();
    }

    /**
     * Waits for an item with @id to change its state to non-placeholder under
     * @container, but at most @timeout seconds.
     *
     * @param container The container to watch for changes
     * @param id The child id to look for
     * @param timeout Seconds to wait befor cancelling
     */
    private async void wait_for_item (MediaContainer container,
                                      string         id,
                                      uint           timeout) {
        MediaFileItem item = null;

        while (item == null || item.place_holder) {
            try {
                item = (yield container.find_object (id,
                                                     this.cancellable))
                                                     as MediaFileItem;
            } catch (Error error) {
                // Handle
                break;
            }

            // This means that either someone externally has removed the item
            // or that the back-end decided it's not a shareable item anymore.
            if (item == null) {
                warning ("Item %s disappeared, stop waiting for it", id);

                break;
            }

            if (item.place_holder) {
                uint source_id = 0;
                source_id = Timeout.add_seconds (timeout, () => {
                    debug ("Timeout on waiting for 'updated' signal on '%s'.",
                           container.id);
                    source_id = 0;
                    this.wait_for_item.callback ();

                    return false;
                });

                var update_id = container.container_updated.connect (() => {
                    debug ("Finished waiting for update signal from container '%s'",
                           container.id);

                        wait_for_item.callback ();
                });

                yield;

                container.disconnect (update_id);

                if (source_id != 0) {
                    Source.remove (source_id);
                } else {
                    break;
                }
            }
        }
    }

    private async void finalize_post () {
        try {
            this.stream.close (this.cancellable);
        } catch (Error error) {
            this.end (Status.INTERNAL_SERVER_ERROR);
            this.handle_continue ();

            return;
        }

        this.server.pause_message (this.msg);

        debug ("Waiting for update signal from container '%s' after pushing" +
               " content to its child item '%s'ā€¦",
               this.object.parent.id,
               this.object.id);

        try {
            this.dotfile.move (this.file,
                               FileCopyFlags.NONE,
                               this.cancellable);
        } catch (Error move_error) {
            // translators: Dotfile is the filename with prefix "."
            warning (_("Failed to move dotfile %s: %s"),
                     this.dotfile.get_uri (),
                     move_error.message);

            this.server.unpause_message (this.msg);
            this.end (Status.INTERNAL_SERVER_ERROR);
            this.handle_continue ();

            return;
        }

        yield wait_for_item (this.object.parent, this.object.id, 5);

        this.server.unpause_message (this.msg);
        this.end (Status.OK);
        this.handle_continue ();
    }

    private void on_got_chunk (ServerMessage msg, Bytes chunk) {
        try {
            var data = chunk.get_data ();
            this.stream.write_all (data, null, this.cancellable);
        } catch (Error error) {
            this.disconnect_message_signals ();
            this.handle_error (
                new HTTPRequestError.INTERNAL_SERVER_ERROR (error.message));
            this.handle_continue ();
        }
    }

    private void on_request_cancelled () {
        this.remove_item.begin ();
    }

    private async void remove_item () {
        var queue = ObjectRemovalQueue.get_default ();
        yield queue.remove_now (this.object as MediaFileItem, null);
    }

    private void disconnect_message_signals () {
        this.msg.got_body.disconnect (this.on_got_body);
        this.msg.got_chunk.disconnect (this.on_got_chunk);
    }

}