Exports user photos as zip file

This commit is contained in:
Marcelo Briones 2015-02-19 00:04:25 -03:00
parent ba7f0cf2ed
commit b154d87070
19 changed files with 288 additions and 50 deletions

View file

@ -148,10 +148,15 @@ class UsersController < ApplicationController
end
def export_photos
tar_path = PhotoMover::move_photos(current_user)
send_data( File.open(tar_path).read, :filename => "#{current_user.id}.tar" )
current_user.queue_export_photos
flash[:notice] = I18n.t('users.edit.export_photos_in_progress')
redirect_to edit_user_path
end
def download_photos
redirect_to current_user.exported_photos_file.url
end
def user_photo
username = params[:username].split('@')[0]
user = User.find_by_username(username)

View file

@ -19,4 +19,21 @@ class ExportMailer < ActionMailer::Base
end
end
def export_photos_complete_for(user)
@user = user
mail(to: @user.email, subject: I18n.t('notifier.export_photos_email.subject', name: @user.name)) do |format|
format.html { render 'users/export_photos_email' }
format.text { render 'users/export_photos_email' }
end
end
def export_photos_failure_for(user)
@user = user
mail(to: @user.email, subject: I18n.t('notifier.export_photos_failure_email.subject', name: @user.name)) do |format|
format.html { render 'users/export_photos_failure_email' }
format.text { render 'users/export_photos_failure_email' }
end
end
end

View file

@ -314,6 +314,40 @@ class User < ActiveRecord::Base
ActiveSupport::Gzip.compress Diaspora::Exporter.new(self).execute
end
######### Photos export ##################
mount_uploader :exported_photos_file, ExportedPhotos
def queue_export_photos
update exporting_photos: true
Workers::ExportPhotos.perform_async(id)
end
def perform_export_photos!
temp_zip = Tempfile.new([username, '_photos.zip'])
begin
Zip::ZipOutputStream.open(temp_zip.path) do |zos|
photos.each do |photo|
begin
photo_data = photo.unprocessed_image.file.read
zos.put_next_entry(photo.remote_photo_name)
zos.print(photo_data)
rescue Errno::ENOENT
logger.info "Export photos error: #{photo.unprocessed_image.file.path} not found"
end
end
end
ensure
temp_zip.close
end
begin
update exported_photos_file: temp_zip, exported_photos_at: Time.zone.now if temp_zip.present?
ensure
restore_attributes if invalid? || temp_zip.present?
update exporting_photos: false
end
end
######### Mailer #######################
def mail(job, *args)
pref = job.to_s.gsub('Workers::Mail::', '').underscore
@ -534,6 +568,8 @@ class User < ActiveRecord::Base
"created_at", "updated_at", "locked_at",
"serialized_private_key", "getting_started",
"disable_mail", "show_community_spotlight_in_stream",
"strip_exif", "email", "remove_after", "export", "exporting", "exported_at"]
"strip_exif", "email", "remove_after",
"export", "exporting", "exported_at",
"exported_photos_file", "exporting_photos", "exported_photos_at"]
end
end

View file

@ -0,0 +1,21 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
class ExportedPhotos < CarrierWave::Uploader::Base
def store_dir
"uploads/users"
end
def filename
"#{model.username}_photos_#{secure_token}.zip" if original_filename.present?
end
protected
def secure_token(bytes = 16)
var = :"@#{mounted_as}_secure_token"
model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.urlsafe_base64(bytes))
end
end

View file

@ -182,23 +182,32 @@
%h3
= t('.export_data')
- if current_user.exporting
.small-horizontal-spacer
.export-in-progress= t('.export_in_progress')
.export-in-progress= t('.export_in_progress')
- elsif current_user.export.present?
.small-horizontal-spacer
= link_to t('.download_export'), download_profile_user_path, class: "button"
.small-horizontal-spacer
= link_to t('.download_export'), download_profile_user_path, class: "btn btn-success"
%h6
= t('.last_exported_at', timestamp: current_user.exported_at)
= link_to t('.request_export_update'), export_profile_user_path, class: "btn"
- else
= link_to t('.request_export'), export_profile_user_path, :class => "btn"
- if current_user.exporting_photos
.small-horizontal-spacer
= link_to t('.request_export_update'), export_profile_user_path
.export-in-progress= t('.export_photos_in_progress')
- elsif current_user.exported_photos_file.present?
.small-horizontal-spacer
= link_to t('.download_export_photos'), download_photos_user_path, class: "btn btn-success"
%h6
= t('.last_exported_at', timestamp: current_user.exported_photos_at)
= link_to t('.request_export_photos_update'), export_photos_user_path, class: "btn"
- else
.small-horizontal-spacer
= link_to t('.request_export'), export_profile_user_path, :class => "button"
= link_to t('.request_export_photos'), export_photos_user_path, :class => "btn"
.span6
%h3
= t('.close_account_text')
=link_to t('.close_account_text'), '#close_account_pane', :rel => 'facebox', :class => "button", :id => "close_account"
=link_to t('.close_account_text'), '#close_account_pane', :rel => 'facebox', :class => "btn btn-danger", :id => "close_account"
.hidden#close_account_pane{:rel => 'facebox'}
#inner_account_delete

View file

@ -162,23 +162,31 @@
%h4
= t('.export_data')
- if current_user.exporting
.small-horizontal-spacer
.export-in-progress= t('.export_in_progress')
.export-in-progress= t('.export_in_progress')
- elsif current_user.export.present?
.small-horizontal-spacer
= link_to t('.download_export'), download_profile_user_path, class: "button"
.small-horizontal-spacer
= link_to t('.download_export'), download_profile_user_path, class: "btn btn-success"
%h6
= t('.last_exported_at', timestamp: current_user.exported_at)
.small-horizontal-spacer
= link_to t('.request_export_update'), export_profile_user_path
= link_to t('.request_export_update'), export_profile_user_path, class: "btn"
- else
.small-horizontal-spacer
= link_to t('.request_export'), export_profile_user_path, :class => "button"
= link_to t('.request_export'), export_profile_user_path, :class => "btn"
%br
%br
- if current_user.exporting_photos
.export-in-progress= t('.export_photos_in_progress')
- elsif current_user.exported_photos_file.present?
= link_to t('.download_export_photos'), download_photos_user_path, class: "btn btn-success"
%h6
= t('.last_exported_at', timestamp: current_user.exported_photos_at)
= link_to t('.request_export_photos_update'), export_photos_user_path, class: "btn"
- else
= link_to t('.request_export_photos'), export_photos_user_path, :class => "btn"
%hr
.span-5.last
%h4
= t('.close_account_text')
=link_to t('.close_account_text'), '#close_account_pane', :rel => 'facebox', :class => "btn", :id => "close_account"
=link_to t('.close_account_text'), '#close_account_pane', :rel => 'facebox', :class => "btn btn-danger", :id => "close_account"
.hidden#close_account_pane{:rel => 'facebox'}
#inner_account_delete
@ -213,3 +221,4 @@
= f.password_field :current_password, :id => :close_account_password
%p
= f.submit t('.close_account_text'), :id => "close_account_confirm", :data => { :confirm => t('are_you_sure_delete_account') }
%br

View file

@ -0,0 +1 @@
<%= t('notifier.export_photos_email.body', url: download_photos_user_url, name: @user.first_name) %>

View file

@ -0,0 +1 @@
<%= t('notifier.export_photos_failure_email.body', name: @user.first_name) %>

View file

@ -0,0 +1,21 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module Workers
class ExportPhotos < Base
sidekiq_options queue: :export_photos
def perform(user_id)
@user = User.find(user_id)
@user.perform_export_photos!
if @user.reload.exported_photos_file.present?
ExportMailer.export_photos_complete_for(@user)
else
ExportMailer.export_photos_failure_for(@user)
end
end
end
end

View file

@ -13,7 +13,6 @@ require 'typhoeus'
require 'post_presenter'
# Our libs
require 'collect_user_photos'
require 'diaspora'
require 'direction_detector'
require 'email_inviter'

View file

@ -817,8 +817,29 @@ en:
Weve encountered an issue while processing your personal data for download.
Please try again!
Sorry,
The diaspora* email robot!
export_photos_email:
subject: "Your photos are ready for download, %{name}"
body: |-
Hello %{name},
Your photos have been processed and are ready for download by following [this link](%{url}).
Cheers,
The diaspora* email robot!
export_photos_failure_email:
subject: "There was an issue with your photos, %{name}"
body: |-
Hello %{name}
Weve encountered an issue while processing your photos for download.
Please try again!
Sorry,
The diaspora* email robot!
accept_invite: "Accept your diaspora* invite!"
invited_you: "%{name} invited you to diaspora*"
@ -1319,6 +1340,11 @@ en:
export_data: "Export data"
export_in_progress: "We are currently processing your data. Please check back in a few moments."
last_exported_at: "(Last updated at %{timestamp})"
download_export_photos: "Download my photos"
request_export_photos: "Request my photos"
request_export_photos_update: "Refresh my photos"
download_photos: "Download my photos"
export_photos_in_progress: "We are currently processing your photos. Please check back in a few moments."
close_account:
dont_go: "Hey, please dont go!"

View file

@ -104,6 +104,7 @@ Diaspora::Application.routes.draw do
get :export_profile
get :download_profile
get :export_photos
get :download_photos
end
controller :users do

View file

@ -0,0 +1,13 @@
class AddPhotosExportToUser < ActiveRecord::Migration
def up
add_column :users, :exported_photos_file, :string
add_column :users, :exported_photos_at, :datetime
add_column :users, :exporting_photos, :boolean, default: false
end
def down
remove_column :users, :exported_photos_file
remove_column :users, :exported_photos_at
remove_column :users, :exporting_photos
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150209230946) do
ActiveRecord::Schema.define(version: 20150220001357) do
create_table "account_deletions", force: :cascade do |t|
t.string "diaspora_handle", limit: 255
@ -562,6 +562,9 @@ ActiveRecord::Schema.define(version: 20150209230946) do
t.datetime "exported_at"
t.boolean "exporting", default: false
t.boolean "strip_exif", default: true
t.string "exported_photos_file", limit: 255
t.datetime "exported_photos_at"
t.boolean "exporting_photos", default: false
end
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree

View file

@ -1,25 +0,0 @@
module PhotoMover
def self.move_photos(user)
Dir.chdir Rails.root
temp_dir = "tmp/exports/#{user.id}"
FileUtils::mkdir_p temp_dir
Dir.chdir 'tmp/exports'
photos = user.visible_shareables(Post).where(:author_id => user.person_id, :type => 'Photo')
photos_dir = "#{user.id}/photos"
FileUtils::mkdir_p photos_dir
photos.each do |photo|
current_photo_location = "#{Rails.root}/public/uploads/images/#{photo.remote_photo_name}"
new_photo_location = "#{photos_dir}/#{photo.remote_photo_name}"
FileUtils::cp current_photo_location, new_photo_location
end
`tar c #{user.id} > #{user.id}.tar`
#system("tar", "c", "#{user.id}",">", "#{user.id}.tar")
FileUtils::rm_r "#{user.id.to_s}/", :secure => true, :force => true
"#{Rails.root}/#{temp_dir}.tar"
end
end

View file

@ -30,9 +30,19 @@ describe UsersController, :type => :controller do
end
describe '#export_photos' do
it 'returns a tar file' do
it 'queues an export photos job' do
expect(@user).to receive :queue_export_photos
get :export_photos
expect(response.header["Content-Type"]).to include "application/octet-stream"
expect(request.flash[:notice]).to eql(I18n.t('users.edit.export_photos_in_progress'))
expect(response).to redirect_to(edit_user_path)
end
end
describe '#download_photos' do
it "redirects to user's photos zip file" do
@user.perform_export_photos!
get :download_photos
expect(response).to redirect_to(@user.exported_photos_file.url)
end
end

View file

@ -34,4 +34,34 @@ describe ExportMailer, :type => :mailer do
expect(ActionMailer::Base.deliveries[0].to[0]).to include(alice.email)
end
end
describe '#export_photos_complete_for' do
it "should deliver successfully" do
expect { ExportMailer.export_photos_complete_for(alice).deliver_now }.to_not raise_error
end
it "should be added to the delivery queue" do
expect { ExportMailer.export_photos_complete_for(alice).deliver_now }.to change(ActionMailer::Base.deliveries, :size).by(1)
end
it "should include correct recipient" do
ExportMailer.export_photos_complete_for(alice).deliver_now
expect(ActionMailer::Base.deliveries[0].to[0]).to include(alice.email)
end
end
describe '#export_photos_failure_for' do
it "should deliver successfully" do
expect { ExportMailer.export_photos_failure_for(alice).deliver_now }.to_not raise_error
end
it "should be added to the delivery queue" do
expect { ExportMailer.export_photos_failure_for(alice).deliver_now }.to change(ActionMailer::Base.deliveries, :size).by(1)
end
it "should include correct recipient" do
ExportMailer.export_photos_failure_for(alice).deliver_now
expect(ActionMailer::Base.deliveries[0].to[0]).to include(alice.email)
end
end
end

View file

@ -1035,6 +1035,41 @@ describe User, :type => :model do
end
end
describe "queue_export_photos" do
it "queues up a job to perform the export photos" do
user = FactoryGirl.create :user
expect(Workers::ExportPhotos).to receive(:perform_async).with(user.id)
user.queue_export_photos
expect(user.exporting_photos).to be_truthy
end
end
describe "perform_export_photos!" do
before do
@user = alice
filename = 'button.png'
image = File.join(File.dirname(__FILE__), '..', 'fixtures', filename)
@saved_image = @user.build_post(:photo, :user_file => File.open(image), :to => alice.aspects.first.id)
@saved_image.save!
end
it "saves a zip export to the user" do
@user.perform_export_photos!
expect(@user.exported_photos_file).to be_present
expect(@user.exported_photos_at).to be_present
expect(@user.exporting_photos).to be_falsey
expect(@user.exported_photos_file.filename).to match /.zip/
expect(Zip::ZipFile.open(@user.exported_photos_file.path).entries.count).to eq(1)
end
it "does not add empty entries when photo not found" do
File.unlink @user.photos.first.unprocessed_image.path
@user.perform_export_photos!
expect(@user.exported_photos_file.filename).to match /.zip/
expect(Zip::ZipFile.open(@user.exported_photos_file.path).entries.count).to eq(0)
end
end
describe "sign up" do
before do
params = {:username => "ohai",

View file

@ -0,0 +1,26 @@
require 'spec_helper'
describe Workers::ExportPhotos do
before do
allow(User).to receive(:find).with(alice.id).and_return(alice)
end
it 'calls export_photos! on user with given id' do
expect(alice).to receive(:perform_export_photos!)
Workers::ExportPhotos.new.perform(alice.id)
end
it 'sends a success message when the export photos is successful' do
allow(alice).to receive(:exported_photos_file).and_return(OpenStruct.new)
expect(ExportMailer).to receive(:export_photos_complete_for).with(alice).and_call_original
Workers::ExportPhotos.new.perform(alice.id)
end
it 'sends a failure message when the export photos fails' do
allow(alice).to receive(:exported_photos_file).and_return(nil)
expect(alice).to receive(:perform_export_photos!).and_return(false)
expect(ExportMailer).to receive(:export_photos_failure_for).with(alice).and_call_original
Workers::ExportPhotos.new.perform(alice.id)
end
end