1 //
2 // Copyright (C) 2001-2013 Graeme Walker <graeme_walker@users.sourceforge.net>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 // ===
17 //
18 // gpopstore.cpp
19 //
20 
21 #include "gdef.h"
22 #include "gpop.h"
23 #include "gpopstore.h"
24 #include "gstr.h"
25 #include "gfile.h"
26 #include "gdirectory.h"
27 #include "gmemory.h"
28 #include "gtest.h"
29 #include "groot.h"
30 #include "gassert.h"
31 #include <sstream>
32 #include <fstream>
33 
34 namespace GPop
35 {
36 	struct FileReader ;
37 	struct DirectoryReader ;
38 	struct FileDeleter ;
39 }
40 
41 /// \class GPop::FileReader
42 /// A trivial class which is used like G::Root by GPop::Store for reading files.
43 ///  The implementation does nothing because files in the pop store are group-readable.
44 ///
45 struct GPop::FileReader
46 {
FileReaderGPop::FileReader47 	FileReader() {}
48 } ;
49 
50 /// \class GPop::DirectoryReader
51 /// A trivial class which is used like G::Root by GPop::Store for reading
52 ///  directory listings.
53 ///
54 struct GPop::DirectoryReader : private G::Root
55 {
DirectoryReaderGPop::DirectoryReader56 	DirectoryReader() {}
57 } ;
58 
59 /// \class GPop::FileDeleter
60 /// A trivial specialisation of G::Root used by GPop::Store for deleting files.
61 ///  The specialisation is not really necessary because the pop store directory is group-writeable.
62 ///
63 struct GPop::FileDeleter : private G::Root
64 {
65 } ;
66 
67 // ==
68 
Store(G::Path path,bool by_name,bool allow_delete)69 GPop::Store::Store( G::Path path , bool by_name , bool allow_delete ) :
70 	m_path(path) ,
71 	m_by_name(by_name) ,
72 	m_allow_delete(allow_delete)
73 {
74 	checkPath( path , by_name , allow_delete ) ;
75 }
76 
dir() const77 G::Path GPop::Store::dir() const
78 {
79 	return m_path ;
80 }
81 
allowDelete() const82 bool GPop::Store::allowDelete() const
83 {
84 	return m_allow_delete ;
85 }
86 
byName() const87 bool GPop::Store::byName() const
88 {
89 	return m_by_name ;
90 }
91 
checkPath(G::Path dir_path,bool by_name,bool allow_delete)92 void GPop::Store::checkPath( G::Path dir_path , bool by_name , bool allow_delete )
93 {
94 	if( by_name )
95 	{
96 		if( !valid(dir_path,false) )
97 			throw InvalidDirectory() ;
98 
99 		G::DirectoryList iter ;
100 		{
101 			DirectoryReader claim_reader ;
102 			iter.readAll( dir_path ) ;
103 		}
104 
105 		int n = 0 ;
106 		while( iter.more() )
107 		{
108 			if( iter.isDir() )
109 			{
110 				n++ ;
111 				if( !valid(iter.filePath(),allow_delete) )
112 				{
113 					; // no-op -- warning only
114 				}
115 			}
116 		}
117 		if( n == 0 )
118 		{
119 			G_WARNING( "GPop::Store: no sub-directories for pop-by-name found in \"" << dir_path << "\": "
120 				<< "create one sub-directory for each authorised pop account" ) ;
121 		}
122 	}
123 	else if( !valid(dir_path,allow_delete) )
124 	{
125 		throw InvalidDirectory() ;
126 	}
127 }
128 
valid(G::Path dir_path,bool allow_delete)129 bool GPop::Store::valid( G::Path dir_path , bool allow_delete )
130 {
131 	G::Directory dir_test( dir_path ) ;
132 	bool ok = false ;
133 	if( allow_delete )
134 	{
135 		std::string tmp = G::Directory::tmp() ;
136 		FileDeleter claim_deleter ;
137 		ok = dir_test.valid() && dir_test.writeable(tmp) ;
138 	}
139 	else
140 	{
141 		FileReader claim_reader ;
142 		ok = dir_test.valid() ;
143 	}
144 	if( !ok )
145 	{
146 		const char * op = allow_delete ? "writing" : "reading" ;
147 		G_WARNING( "GPop::Store: directory not valid for " << op << ": \"" << dir_path << "\"" ) ;
148 	}
149 	return ok ;
150 }
151 
152 // ===
153 
File(const G::Path & content_path)154 GPop::StoreLock::File::File( const G::Path & content_path ) :
155 	name(content_path.basename()) ,
156 	size(toSize(G::File::sizeString(content_path.str())))
157 {
158 }
159 
File(const std::string & content_name,const std::string & size_string)160 GPop::StoreLock::File::File( const std::string & content_name , const std::string & size_string ) :
161 	name(content_name) ,
162 	size(toSize(size_string))
163 {
164 }
165 
operator <(const File & rhs) const166 bool GPop::StoreLock::File::operator<( const File & rhs ) const
167 {
168 	return name < rhs.name ;
169 }
170 
toSize(const std::string & s)171 GPop::StoreLock::Size GPop::StoreLock::File::toSize( const std::string & s )
172 {
173 	return G::Str::toULong( s , true ) ;
174 }
175 
176 // ===
177 
StoreLock(Store & store)178 GPop::StoreLock::StoreLock( Store & store ) :
179 	m_store(&store)
180 {
181 }
182 
lock(const std::string & user)183 void GPop::StoreLock::lock( const std::string & user )
184 {
185 	G_ASSERT( ! locked() ) ;
186 	G_ASSERT( ! user.empty() ) ;
187 	G_ASSERT( m_store != NULL ) ;
188 
189 	m_user = user ;
190 	m_dir = m_store->dir() ;
191 	if( m_store->byName() )
192 		m_dir.pathAppend( user ) ;
193 
194 	// build a read-only list of files (inc. file sizes)
195 	{
196 		DirectoryReader claim_reader ;
197 		G::DirectoryList iter ;
198 		iter.readType( m_dir , ".envelope" ) ;
199 		while( iter.more() )
200 		{
201 			File file( contentPath(iter.fileName().str()) ) ;
202 			m_initial.insert( file ) ;
203 		}
204 	}
205 
206 	if( G::Test::enabled("large-pop-list") )
207 	{
208 		// create a larger list
209 		size_t limit = m_initial.size() * 1000U ;
210 		for( size_t i = 0U ; i < limit ; i++ )
211 		{
212 			std::ostringstream ss ;
213 			ss << "dummy." << i << ".content" ;
214 			m_initial.insert( File(ss.str()) ) ;
215 		}
216 	}
217 
218 	// take a mutable copy
219 	m_current = m_initial ;
220 
221 	G_ASSERT( locked() ) ;
222 }
223 
locked() const224 bool GPop::StoreLock::locked() const
225 {
226 	return m_store != NULL && ! m_user.empty() ;
227 }
228 
~StoreLock()229 GPop::StoreLock::~StoreLock()
230 {
231 }
232 
messageCount() const233 GPop::StoreLock::Size GPop::StoreLock::messageCount() const
234 {
235 	G_ASSERT( locked() ) ;
236 	return m_current.size() ;
237 }
238 
totalByteCount() const239 GPop::StoreLock::Size GPop::StoreLock::totalByteCount() const
240 {
241 	G_ASSERT( locked() ) ;
242 	Size total = 0 ;
243 	for( Set::const_iterator p = m_current.begin() ; p != m_current.end() ; ++p )
244 		total += (*p).size ;
245 	return total ;
246 }
247 
valid(int id) const248 bool GPop::StoreLock::valid( int id ) const
249 {
250 	G_ASSERT( locked() ) ;
251 	return id >= 1 && id <= static_cast<int>(m_initial.size()) ;
252 }
253 
find(int id)254 GPop::StoreLock::Set::iterator GPop::StoreLock::find( int id )
255 {
256 	G_ASSERT( valid(id) ) ;
257 	Set::iterator initial_p = m_initial.begin() ;
258 	for( int i = 1 ; i < id && initial_p != m_initial.end() ; i++ , ++initial_p ) ;
259 	return initial_p ;
260 }
261 
find(int id) const262 GPop::StoreLock::Set::const_iterator GPop::StoreLock::find( int id ) const
263 {
264 	G_ASSERT( valid(id) ) ;
265 	Set::const_iterator initial_p = m_initial.begin() ;
266 	for( int i = 1 ; i < id && initial_p != m_initial.end() ; i++ , ++initial_p ) ;
267 	return initial_p ;
268 }
269 
find(const std::string & name)270 GPop::StoreLock::Set::iterator GPop::StoreLock::find( const std::string & name )
271 {
272 	Set::iterator current_p = m_current.begin() ;
273 	for( ; current_p != m_current.end() ; ++current_p )
274 	{
275 		if( (*current_p).name == name )
276 			break ;
277 	}
278 	return current_p ;
279 }
280 
byteCount(int id) const281 GPop::StoreLock::Size GPop::StoreLock::byteCount( int id ) const
282 {
283 	G_ASSERT( locked() ) ;
284 	return (*find(id)).size ;
285 }
286 
list(int id) const287 GPop::StoreLock::List GPop::StoreLock::list( int id ) const
288 {
289 	G_ASSERT( locked() ) ;
290 	List list ;
291 	int i = 1 ;
292 	for( Set::const_iterator p = m_current.begin() ; p != m_current.end() ; ++p , i++ )
293 	{
294 		if( id == -1 || id == i )
295 			list.push_back( Entry(i,(*p).size,(*p).name) ) ;
296 	}
297 	return list ;
298 }
299 
get(int id) const300 std::auto_ptr<std::istream> GPop::StoreLock::get( int id ) const
301 {
302 	G_ASSERT( locked() ) ;
303 	G_ASSERT( valid(id) ) ;
304 
305 	G_DEBUG( "GPop::StoreLock::get: " << id << ": " << path(id) ) ;
306 
307 	std::auto_ptr<std::ifstream> file ;
308 	{
309 		FileReader claim_reader ;
310 		file <<= new std::ifstream( path(id).str().c_str() , std::ios_base::binary | std::ios_base::in ) ;
311 	}
312 
313 	if( ! file->good() )
314 		throw CannotRead( path(id).str() ) ;
315 
316 	return std::auto_ptr<std::istream>( file.release() ) ;
317 }
318 
remove(int id)319 void GPop::StoreLock::remove( int id )
320 {
321 	G_ASSERT( locked() ) ;
322 	G_ASSERT( valid(id) ) ;
323 
324 	Set::iterator initial_p = find( id ) ;
325 	Set::iterator current_p = find( (*initial_p).name ) ;
326 	if( current_p != m_current.end() )
327 	{
328 		m_deleted.insert( *initial_p ) ;
329 		m_current.erase( current_p ) ;
330 	}
331 }
332 
commit()333 void GPop::StoreLock::commit()
334 {
335 	G_ASSERT( locked() ) ;
336 	if( m_store )
337 	{
338 		Store * store = m_store ;
339 		m_store = NULL ;
340 		doCommit( *store ) ;
341 	}
342 	m_store = NULL ;
343 }
344 
doCommit(Store & store) const345 void GPop::StoreLock::doCommit( Store & store ) const
346 {
347 	bool all_ok = true ;
348 	for( Set::const_iterator p = m_deleted.begin() ; p != m_deleted.end() ; ++p )
349 	{
350 		if( store.allowDelete() )
351 		{
352 			deleteFile( envelopePath(*p) , all_ok ) ;
353 			if( unlinked(store,*p) ) // race condition could leave content files undeleted
354 				deleteFile( contentPath(*p) , all_ok ) ;
355 		}
356 		else
357 		{
358 			G_DEBUG( "StoreLock::doCommit: not deleting \"" << (*p).name << "\"" ) ;
359 		}
360 	}
361 	if( ! all_ok )
362 		throw CannotDelete() ;
363 }
364 
deleteFile(const G::Path & path,bool & all_ok) const365 void GPop::StoreLock::deleteFile( const G::Path & path , bool & all_ok ) const
366 {
367 	bool ok = false ;
368 	{
369 		FileDeleter claim_deleter ;
370 		ok = G::File::remove( path , G::File::NoThrow() ) ;
371 	}
372 	all_ok = ok && all_ok ;
373 	if( ! ok )
374 		G_ERROR( "StoreLock::remove: failed to delete " << path ) ;
375 }
376 
uidl(int id) const377 std::string GPop::StoreLock::uidl( int id ) const
378 {
379 	G_ASSERT( valid(id) ) ;
380 	Set::const_iterator p = find(id) ;
381 	return (*p).name ;
382 }
383 
path(int id) const384 G::Path GPop::StoreLock::path( int id ) const
385 {
386 	G_ASSERT( valid(id) ) ;
387 	Set::const_iterator p = find(id) ;
388 	const File & file = (*p) ;
389 	return contentPath( file ) ;
390 }
391 
path(const std::string & filename,bool fallback) const392 G::Path GPop::StoreLock::path( const std::string & filename , bool fallback ) const
393 {
394 	// expected path
395 	G::Path path_1 = m_dir ;
396 	path_1.pathAppend( filename ) ;
397 
398 	// or fallback to the parent directory
399 	G::Path path_2 = m_dir ; path_2.pathAppend("..") ;
400 	path_2.pathAppend( filename ) ;
401 
402 	return ( fallback && !G::File::exists(path_1,G::File::NoThrow()) ) ? path_2 : path_1 ;
403 }
404 
envelopeName(const std::string & content_name) const405 std::string GPop::StoreLock::envelopeName( const std::string & content_name ) const
406 {
407 	std::string filename = content_name ;
408 	G::Str::replace( filename , "content" , "envelope" ) ;
409 	return filename ;
410 }
411 
contentName(const std::string & envelope_name) const412 std::string GPop::StoreLock::contentName( const std::string & envelope_name ) const
413 {
414 	std::string filename = envelope_name ;
415 	G::Str::replace( filename , "envelope" , "content" ) ;
416 	return filename ;
417 }
418 
contentPath(const std::string & envelope_name) const419 G::Path GPop::StoreLock::contentPath( const std::string & envelope_name ) const
420 {
421 	const bool try_parent_directory = true ;
422 	return path( contentName(envelope_name) , try_parent_directory ) ;
423 }
424 
contentPath(const File & file) const425 G::Path GPop::StoreLock::contentPath( const File & file ) const
426 {
427 	const bool try_parent_directory = true ;
428 	return path( file.name , try_parent_directory ) ;
429 }
430 
envelopePath(const File & file) const431 G::Path GPop::StoreLock::envelopePath( const File & file ) const
432 {
433 	const bool try_parent_directory = false ;
434 	return path( envelopeName(file.name) , try_parent_directory ) ;
435 }
436 
rollback()437 void GPop::StoreLock::rollback()
438 {
439 	G_ASSERT( locked() ) ;
440 	m_deleted.clear() ;
441 	m_current = m_initial ;
442 }
443 
unlinked(Store & store,const File & file) const444 bool GPop::StoreLock::unlinked( Store & store , const File & file ) const
445 {
446 	if( !store.byName() )
447 	{
448 		G_DEBUG( "StoreLock::unlinked: unlinked since not pop-by-name: " << file.name ) ;
449 		return true ;
450 	}
451 
452 	G::Path normal_content_path = m_dir ; normal_content_path.pathAppend( file.name ) ;
453 	if( G::File::exists(normal_content_path,G::File::NoThrow()) )
454 	{
455 		G_DEBUG( "StoreLock::unlinked: unlinked since in its own directory: " << normal_content_path ) ;
456 		return true ;
457 	}
458 
459 	// look for corresponding envelopes in all child directories
460 	bool found = false ;
461 	{
462 		G::DirectoryList iter ;
463 		{
464 			DirectoryReader claim_reader ;
465 			iter.readAll( store.dir() ) ;
466 		}
467 		while( iter.more() )
468 		{
469 			if( ! iter.isDir() ) continue ;
470 			G_DEBUG( "Store::unlinked: checking sub-directory: " << iter.fileName() ) ;
471 			G::Path envelope_path = iter.filePath() ; envelope_path.pathAppend(envelopeName(file.name)) ;
472 			if( G::File::exists(envelope_path,G::File::NoThrow()) )
473 			{
474 				G_DEBUG( "StoreLock::unlinked: still in use: envelope exists: " << envelope_path ) ;
475 				found = true ;
476 				break ;
477 			}
478 		}
479 	}
480 
481 	if( ! found )
482 	{
483 		G_DEBUG( "StoreLock::unlinked: unlinked since no envelope found in any sub-directory" ) ;
484 		return true ;
485 	}
486 
487 	return false ;
488 }
489 
490