Beispiel #1
0
        public void Put(string remotename, System.IO.Stream stream)
        {
            try
            {
                Google.Documents.Document folder = GetFolder();

                //Special, since uploads can overwrite or create,
                // we must figure out if the file exists in advance.
                //Unfortunately it would be wastefull to request the list
                // for each upload request, so we rely on the cache being
                // correct

                TaggedFileEntry doc = null;
                if (m_files == null)
                {
                    doc = TryGetFile(remotename);
                }
                else
                {
                    m_files.TryGetValue(remotename, out doc);
                }

                try
                {
                    string resumableUri;
                    if (doc != null)
                    {
                        if (doc.MediaUrl == null)
                        {
                            //Strange, we could not get the edit url, perhaps it is readonly?
                            //Fallback strategy is "delete-then-upload"
                            try { this.Delete(remotename); }
                            catch { }

                            doc = TryGetFile(remotename);
                            if (doc != null || doc.MediaUrl == null)
                            {
                                throw new Exception(string.Format(Strings.GoogleDocs.FileIsReadOnlyError, remotename));
                            }
                        }
                    }


                    //File does not exist, we upload a new one
                    if (doc == null)
                    {
                        //First we need to get a resumeable upload url
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create("https://docs.google.com/feeds/upload/create-session/default/private/full/" + System.Web.HttpUtility.UrlEncode(folder.ResourceId) + "/contents?convert=false");
                        req.Method = "POST";
                        req.Headers.Add("X-Upload-Content-Length", stream.Length.ToString());
                        req.Headers.Add("X-Upload-Content-Type", "application/octet-stream");
                        req.UserAgent = USER_AGENT;
                        req.Headers.Add("GData-Version", "3.0");

                        //Build the atom entry describing the file we want to create
                        string labels = "";
                        foreach (string s in m_labels)
                        {
                            if (s.Trim().Length > 0)
                            {
                                labels += string.Format(ATTRIBUTE_TEMPLATE, s);
                            }
                        }

                        //Apply the name and content-type to the not-yet-uploaded file
                        byte[] data = System.Text.Encoding.UTF8.GetBytes(string.Format(CREATE_ITEM_TEMPLATE, System.Web.HttpUtility.HtmlEncode(remotename), labels));
                        req.ContentLength = data.Length;
                        req.ContentType   = "application/atom+xml";

                        //Authenticate our request
                        m_cla.ApplyAuthenticationToRequest(req);

                        Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
                        using (System.IO.Stream s = areq.GetRequestStream())
                            s.Write(data, 0, data.Length);

                        using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
                        {
                            int code = (int)resp.StatusCode;
                            if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
                            {
                                throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
                            }

                            resumableUri = resp.Headers["Location"];
                        }
                    }
                    else
                    {
                        //First we need to get a resumeable upload url
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(doc.MediaUrl);
                        req.Method = "PUT";
                        req.Headers.Add("X-Upload-Content-Length", stream.Length.ToString());
                        req.Headers.Add("X-Upload-Content-Type", "application/octet-stream");
                        req.UserAgent = USER_AGENT;
                        req.Headers.Add("If-Match", doc.ETag);
                        req.Headers.Add("GData-Version", "3.0");

                        //This is a blank marker request
                        req.ContentLength = 0;
                        //Bad... docs say "text/plain" or "text/xml", but really needs to be content type, otherwise overwrite fails
                        //req.ContentType = "text/plain";
                        req.ContentType = "application/octet-stream";

                        //Authenticate our request
                        m_cla.ApplyAuthenticationToRequest(req);

                        Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
                        using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
                        {
                            int code = (int)resp.StatusCode;
                            if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
                            {
                                throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
                            }

                            resumableUri = resp.Headers["Location"];
                        }
                    }

                    //Ensure that we have a resumeable upload url
                    if (resumableUri == null)
                    {
                        throw new Exception(Strings.GoogleDocs.NoResumeURLError);
                    }

                    string   id      = null;
                    byte[]   buffer  = new byte[8 * 1024];
                    int      retries = 0;
                    long     initialPosition;
                    DateTime initialRequestTime = DateTime.Now;

                    while (stream.Position != stream.Length)
                    {
                        initialPosition = stream.Position;
                        long postbytes = Math.Min(stream.Length - initialPosition, TRANSFER_CHUNK_SIZE);

                        //Post a fragment of the file as a partial request
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(resumableUri);
                        req.Method        = "PUT";
                        req.UserAgent     = USER_AGENT;
                        req.ContentLength = postbytes;
                        req.ContentType   = "application/octet-stream";
                        req.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", initialPosition, initialPosition + (postbytes - 1), stream.Length.ToString()));

                        //Copy the current fragment of bytes
                        Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req);
                        using (System.IO.Stream s = areq.GetRequestStream())
                        {
                            long bytesleft = postbytes;
                            long written   = 0;
                            int  a;
                            while (bytesleft != 0 && ((a = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, bytesleft))) != 0))
                            {
                                s.Write(buffer, 0, a);
                                bytesleft -= a;
                                written   += a;
                            }

                            s.Flush();

                            if (bytesleft != 0 || postbytes != written)
                            {
                                throw new System.IO.EndOfStreamException();
                            }
                        }

                        try
                        {
                            using (HttpWebResponse resp = (HttpWebResponse)areq.GetResponse())
                            {
                                int code = (int)resp.StatusCode;
                                if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically
                                {
                                    throw new System.Net.WebException(resp.StatusDescription, null, System.Net.WebExceptionStatus.ProtocolError, resp);
                                }

                                //If all goes well, we should now get an atom entry describing the new element
                                System.Xml.XmlDocument xml = new XmlDocument();
                                using (System.IO.Stream s = areq.GetResponseStream())
                                    xml.Load(s);

                                System.Xml.XmlNamespaceManager mgr = new XmlNamespaceManager(xml.NameTable);
                                mgr.AddNamespace("atom", "http://www.w3.org/2005/Atom");
                                mgr.AddNamespace("gd", "http://schemas.google.com/g/2005");
                                id = xml.SelectSingleNode("atom:entry/atom:id", mgr).InnerText;
                                string resourceId = xml.SelectSingleNode("atom:entry/gd:resourceId", mgr).InnerText;
                                string url        = xml.SelectSingleNode("atom:entry/atom:content", mgr).Attributes["src"].Value;
                                string mediaUrl   = null;

                                foreach (XmlNode n in xml.SelectNodes("atom:entry/atom:link", mgr))
                                {
                                    if (n.Attributes["rel"] != null && n.Attributes["href"] != null && n.Attributes["rel"].Value.EndsWith("#resumable-edit-media"))
                                    {
                                        mediaUrl = n.Attributes["href"].Value;
                                        break;
                                    }
                                }

                                if (doc == null)
                                {
                                    TaggedFileEntry tf = new TaggedFileEntry(remotename, stream.Length, initialRequestTime, initialRequestTime, resourceId, url, mediaUrl, resp.Headers["ETag"]);
                                    m_files.Add(remotename, tf);
                                }
                                else
                                {
                                    //Since we update an existing item, we just need to update the ETag
                                    doc.ETag = resp.Headers["ETag"];
                                }
                            }
                            retries = 0;
                        }
                        catch (WebException wex)
                        {
                            bool acceptedError =
                                wex.Status == WebExceptionStatus.ProtocolError &&
                                wex.Response is HttpWebResponse &&
                                (int)((HttpWebResponse)wex.Response).StatusCode == 308;

                            //Mono does not give us the response object,
                            // so we rely on the error code being present
                            // in the string, not ideal, but I have found
                            // no other workaround :(
                            if (Duplicati.Library.Utility.Utility.IsMono)
                            {
                                acceptedError |=
                                    wex.Status == WebExceptionStatus.ProtocolError &&
                                    wex.Message.Contains("308");
                            }

                            //Accept the 308 until we are complete
                            if (acceptedError &&
                                initialPosition + postbytes != stream.Length)
                            {
                                retries = 0;
                                //Accept the 308 until we are complete
                            }
                            else
                            {
                                //Retries are handled in Duplicati, but it is much more efficient here,
                                // because we only re-submit the last TRANSFER_CHUNK_SIZE bytes,
                                // instead of the entire file
                                retries++;
                                if (retries > 2)
                                {
                                    throw;
                                }
                                else
                                {
                                    System.Threading.Thread.Sleep(2000 * retries);
                                }

                                stream.Position = initialPosition;
                            }
                        }
                    }

                    if (string.IsNullOrEmpty(id))
                    {
                        throw new Exception(Strings.GoogleDocs.NoIDReturnedError);
                    }
                }
                catch
                {
                    //Clear the cache as we have no idea what happened
                    m_files = null;

                    throw;
                }
            }
            catch (Google.GData.Client.CaptchaRequiredException cex)
            {
                throw new Exception(string.Format(Strings.GoogleDocs.CaptchaRequiredError, CAPTCHA_UNLOCK_URL), cex);
            }
        }