support one-time download
authorLunar <lunar@anargeek.net>
Sat, 7 Aug 2010 13:18:59 +0000 (15:18 +0200)
committerLunar <lunar@anargeek.net>
Sat, 7 Aug 2010 13:23:51 +0000 (15:23 +0200)
README
coquelicot.rb
po/coquelicot.pot
po/fr/coquelicot.po
test_coquelicot.rb
views/index.haml

diff --git a/README b/README
index 43fa382..2490e95 100644 (file)
--- a/README
+++ b/README
@@ -28,6 +28,11 @@ Features
    During a configurable period of time, trying to download the file
    will return a page saying "too late" instead of "not found".
 
+ * Support for one-time download
+
+   An user might want to allow exactly _one_ download of a file,
+   to more closely replace an email attachment.
+
  * Upload progress bar
 
    If the web server tracks upload progress, users having javascript
@@ -119,11 +124,6 @@ Future
    One could like to also configure no password or integrate with
    webmails or other authentication system.
 
- * One-time download
-
-   An user might want to allow exactly _one_ download of a file,
-   to more closely replace an email attachment.
-
  * More flexible expiration
 
    It might be interesting to also offer a calendar for specifying
@@ -179,6 +179,7 @@ Once decrypted, content has the following format:
     Filename: "<original file name>"
     Content-Type: "<MIME type>"
     Length: <file length is bytes>
+    One-time-only: <true|false>
     --- 
     <original bytes forming the file content>
 
index e73d690..95f87e9 100644 (file)
@@ -52,6 +52,25 @@ class StoredFile
     @expire_at < Time.now
   end
 
+  def one_time_only?
+    @meta['One-time-only'] && @meta['One-time-only'] == 'true'
+  end
+
+  def exclusively(&block)
+    old_path = @path
+    begin
+      new_path = "#{old_path}.#{gen_random_base32(16)}"
+    end while File.exists? new_path
+    File.rename(old_path, new_path)
+    @path = new_path
+    File.open(old_path, 'w').close
+    begin
+      yield
+    ensure
+      File.rename(new_path, old_path)
+    end
+  end
+
   def self.create(src, pass, meta)
     salt = gen_salt
     clear_meta = { "Coquelicot" => COQUELICOT_VERSION,
@@ -377,6 +396,7 @@ post '/upload' do
     params[:expire] = options.default_expire
   end
   expire_at = Time.now + 60 * params[:expire].to_i
+  one_time_only = params[:one_time] and params[:one_time] == 'true'
   if params[:file_key].nil? or params[:file_key].empty?then
     pass = gen_random_pass
   else
@@ -386,9 +406,10 @@ post '/upload' do
   link = depot.add_file(
      src, pass,
      { "Expire-at" => expire_at.to_i,
+       "One-time-only" => one_time_only,
        "Filename" => params[:file][:filename],
        "Length" => src.stat.size,
-       "Content-Type" => params[:file][:type]
+       "Content-Type" => params[:file][:type],
      })
   redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
   redirect "ready/#{link}"
@@ -398,11 +419,7 @@ def expired
   throw :halt, [410, haml(:expired)]
 end
 
-def send_stored_file(link, pass)
-  file = depot.get_file(link, pass)
-  return false if file.nil?
-  return expired if file.expired?
-
+def send_stored_file(file)
   last_modified file.created_at.httpdate
   attachment file.meta['Filename']
   response['Content-Length'] = "#{file.meta['Length']}"
@@ -410,10 +427,23 @@ def send_stored_file(link, pass)
   throw :halt, [200, file]
 end
 
+def send_link(link, pass)
+  file = depot.get_file(link, pass)
+  return false if file.nil?
+  return expired if file.expired?
+
+  return send_stored_file(file) unless file.one_time_only?
+
+  file.exclusively do
+    begin  send_stored_file(file)
+    ensure file.empty!            end
+  end
+end
+
 get '/:link-:pass' do |link, pass|
   link = remap_base32_extra_characters(link)
   pass = remap_base32_extra_characters(pass)
-  not_found unless send_stored_file(link, pass)
+  not_found unless send_link(link, pass)
 end
 
 get '/:link' do |link|
@@ -427,7 +457,8 @@ post '/:link' do |link|
   pass = params[:file_key]
   return 403 if pass.nil? or pass.empty?
   begin
-    return 403 unless send_stored_file(link, pass)
+    # send Forbidden even if file is not found
+    return 403 unless send_link(link, pass)
   rescue BadKey => ex
     403
   end
index 23fa97c..5704816 100644 (file)
@@ -7,15 +7,48 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: coquelicot 1.0.0\n"
-"POT-Creation-Date: 2010-08-03 19:12+0200\n"
+"POT-Creation-Date: 2010-08-07 15:23+0200\n"
 "PO-Revision-Date: 2010-08-03 17:15+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
+#: views/ready.haml:1
+msgid "Pass this on!"
+msgstr ""
+
+#: views/layout.haml:1
+msgid "coquelicot"
+msgstr ""
+
+#: views/layout.haml:7
+msgid "Generate random"
+msgstr ""
+
+#: views/layout.haml:7
+msgid "Generating…"
+msgstr ""
+
+#: views/layout.haml:7
+msgid "Coquelicot © 2010 potager.org"
+msgstr ""
+
+#: views/layout.haml:8 views/layout.haml:10
+msgid "—"
+msgstr ""
+
+#: views/layout.haml:9
+msgid "AGPLv3"
+msgstr ""
+
+#: views/layout.haml:11
+msgid "git clone #{base_href}coquelicot.git"
+msgstr ""
+
 #: views/index.haml:1
 msgid "Share a file!"
 msgstr ""
@@ -53,47 +86,15 @@ msgid "1 month"
 msgstr ""
 
 #: views/index.haml:28
-msgid "Download password:"
-msgstr ""
-
-#: views/expired.haml:1
-msgid "Too late…"
-msgstr ""
-
-#: views/expired.haml:2
-msgid "Sorry, file has expired."
-msgstr ""
-
-#: views/layout.haml:1
-msgid "coquelicot"
-msgstr ""
-
-#: views/layout.haml:7
-msgid "Generate random"
+msgid "One time download:"
 msgstr ""
 
-#: views/layout.haml:7
-msgid "Generating…"
-msgstr ""
-
-#: views/layout.haml:7
-msgid "Coquelicot © 2010 potager.org"
-msgstr ""
-
-#: views/layout.haml:8 views/layout.haml:10
-msgid "—"
+#: views/index.haml:32
+msgid "Remove after one download"
 msgstr ""
 
-#: views/layout.haml:9
-msgid "AGPLv3"
-msgstr ""
-
-#: views/layout.haml:11
-msgid "git clone #{base_href}coquelicot.git"
-msgstr ""
-
-#: views/ready.haml:1
-msgid "Pass this on!"
+#: views/index.haml:33
+msgid "Download password:"
 msgstr ""
 
 #: views/enter_file_key.haml:1
@@ -103,3 +104,11 @@ msgstr ""
 #: views/enter_file_key.haml:4
 msgid "Password:"
 msgstr ""
+
+#: views/expired.haml:1
+msgid "Too late…"
+msgstr ""
+
+#: views/expired.haml:2
+msgid "Sorry, file has expired."
+msgstr ""
index 4e7dc45..ee6366d 100644 (file)
@@ -6,15 +6,48 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: coquelicot 1.0.0\n"
-"POT-Creation-Date: 2010-08-03 19:12+0200\n"
+"POT-Creation-Date: 2010-08-07 15:23+0200\n"
 "PO-Revision-Date: 2010-08-03 17:15+0200\n"
 "Last-Translator: potager.org <jardiniers@potager.org>\n"
 "Language-Team: potager.org <jardiniers@potager.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
+#: views/ready.haml:1
+msgid "Pass this on!"
+msgstr "À transmettre !"
+
+#: views/layout.haml:1
+msgid "coquelicot"
+msgstr "coquelicot"
+
+#: views/layout.haml:7
+msgid "Generate random"
+msgstr "Générer aléatoirement"
+
+#: views/layout.haml:7
+msgid "Generating…"
+msgstr "Génération…"
+
+#: views/layout.haml:7
+msgid "Coquelicot © 2010 potager.org"
+msgstr "Coquelicot © 2010 potager.org"
+
+#: views/layout.haml:8 views/layout.haml:10
+msgid "—"
+msgstr "—"
+
+#: views/layout.haml:9
+msgid "AGPLv3"
+msgstr "AGPLv3"
+
+#: views/layout.haml:11
+msgid "git clone #{base_href}coquelicot.git"
+msgstr "git clone #{base_href}coquelicot.git"
+
 #: views/index.haml:1
 msgid "Share a file!"
 msgstr "Partager un fichier !"
@@ -52,48 +85,16 @@ msgid "1 month"
 msgstr "1 mois"
 
 #: views/index.haml:28
-msgid "Download password:"
-msgstr "Passe pour le téléchargement :"
-
-#: views/expired.haml:1
-msgid "Too late…"
-msgstr "Trop tard…"
-
-#: views/expired.haml:2
-msgid "Sorry, file has expired."
-msgstr "Désolé, le fichier a expiré."
-
-#: views/layout.haml:1
-msgid "coquelicot"
-msgstr "coquelicot"
-
-#: views/layout.haml:7
-msgid "Generate random"
-msgstr "Générer aléatoirement"
-
-#: views/layout.haml:7
-msgid "Generating…"
-msgstr "Génération…"
-
-#: views/layout.haml:7
-msgid "Coquelicot © 2010 potager.org"
-msgstr "Coquelicot © 2010 potager.org"
-
-#: views/layout.haml:8 views/layout.haml:10
-msgid "—"
-msgstr "—"
-
-#: views/layout.haml:9
-msgid "AGPLv3"
-msgstr "AGPLv3"
+msgid "One time download:"
+msgstr ""
 
-#: views/layout.haml:11
-msgid "git clone #{base_href}coquelicot.git"
-msgstr "git clone #{base_href}coquelicot.git"
+#: views/index.haml:32
+msgid "Remove after one download"
+msgstr ""
 
-#: views/ready.haml:1
-msgid "Pass this on!"
-msgstr "À transmettre !"
+#: views/index.haml:33
+msgid "Download password:"
+msgstr "Passe pour le téléchargement :"
 
 #: views/enter_file_key.haml:1
 msgid "Enter download password…"
@@ -102,3 +103,11 @@ msgstr "Entrer le passe de téléchargement…"
 #: views/enter_file_key.haml:4
 msgid "Password:"
 msgstr "Passe :"
+
+#: views/expired.haml:1
+msgid "Too late…"
+msgstr "Trop tard…"
+
+#: views/expired.haml:2
+msgid "Sorry, file has expired."
+msgstr "Désolé, le fichier a expiré."
index 02009ee..331c651 100644 (file)
@@ -110,6 +110,16 @@ describe 'Coquelicot' do
     url_name.split('-').should have(1).items
   end
 
+  it "should only allow one time download to be retrieved once" do
+    url = upload :one_time => true
+    get url
+    last_response.should be_ok
+    last_response['Content-Type'].should eql('text/x-script.ruby')
+    last_response.body.should eql(File.new(__FILE__).read)
+    get url
+    last_response.status.should eql(410)
+  end
+
   it "should allow retrieval of a password protected file" do
     url = upload :file_key => 'somethingSecret'
     get url
index 8788279..3694cf8 100644 (file)
       %option{ :value => 60 * 24 * 7  } 1 week
       %option{ :value => 60 * 24 * 30 } 1 month
   .field
+    %label One time download:
+    %input{ :type => 'checkbox', :id => 'one_time', :name => 'one_time', :value => true }
+    %label{ :for => 'one_time' } Remove after one download
+  .field
     %label{ :for => 'file_key' } Download password:
     %input.input{ :type => 'password', :id => 'file_key', :name => 'file_key' }
   .field