1 | /* This file is part of the KDE libraries |
2 | Copyright (C) 2000 David Faure <faure@kde.org> |
3 | |
4 | This library is free software; you can redistribute it and/or |
5 | modify it under the terms of the GNU Library General Public |
6 | License as published by the Free Software Foundation; either |
7 | version 2 of the License, or (at your option) any later version. |
8 | |
9 | This library 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 GNU |
12 | Library General Public License for more details. |
13 | |
14 | You should have received a copy of the GNU Library General Public License |
15 | along with this library; see the file COPYING.LIB. If not, write to |
16 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
17 | Boston, MA 02110-1301, USA. |
18 | */ |
19 | |
20 | #include "kio_archive.h" |
21 | |
22 | #include <sys/types.h> |
23 | #include <sys/stat.h> |
24 | #include <stdlib.h> |
25 | #include <unistd.h> |
26 | |
27 | #include <QFile> |
28 | |
29 | #include <kglobal.h> |
30 | #include <kurl.h> |
31 | #include <kdebug.h> |
32 | #include <kcomponentdata.h> |
33 | #include <ktar.h> |
34 | #include <kzip.h> |
35 | #include <kar.h> |
36 | #include <kmimetype.h> |
37 | #include <klocale.h> |
38 | #include <kde_file.h> |
39 | #include <kio/global.h> |
40 | |
41 | #include <kuser.h> |
42 | |
43 | using namespace KIO; |
44 | |
45 | extern "C" { int KDE_EXPORT kdemain(int argc, char **argv); } |
46 | |
47 | int kdemain( int argc, char **argv ) |
48 | { |
49 | KComponentData componentData( "kio_archive" ); |
50 | |
51 | kDebug(7109) << "Starting" << getpid(); |
52 | |
53 | if (argc != 4) |
54 | { |
55 | fprintf(stderr, "Usage: kio_archive protocol domain-socket1 domain-socket2\n" ); |
56 | exit(-1); |
57 | } |
58 | |
59 | ArchiveProtocol slave(argv[2], argv[3]); |
60 | slave.dispatchLoop(); |
61 | |
62 | kDebug(7109) << "Done" ; |
63 | return 0; |
64 | } |
65 | |
66 | ArchiveProtocol::ArchiveProtocol( const QByteArray &pool, const QByteArray &app ) : SlaveBase( "tar" , pool, app ) |
67 | { |
68 | kDebug( 7109 ) << "ArchiveProtocol::ArchiveProtocol" ; |
69 | m_archiveFile = 0L; |
70 | } |
71 | |
72 | ArchiveProtocol::~ArchiveProtocol() |
73 | { |
74 | delete m_archiveFile; |
75 | } |
76 | |
77 | bool ArchiveProtocol::checkNewFile( const KUrl & url, QString & path, KIO::Error& errorNum ) |
78 | { |
79 | #ifndef Q_WS_WIN |
80 | QString fullPath = url.path(); |
81 | #else |
82 | QString fullPath = url.path().remove(0, 1); |
83 | #endif |
84 | kDebug(7109) << "ArchiveProtocol::checkNewFile" << fullPath; |
85 | |
86 | |
87 | // Are we already looking at that file ? |
88 | if ( m_archiveFile && m_archiveName == fullPath.left(m_archiveName.length()) ) |
89 | { |
90 | // Has it changed ? |
91 | KDE_struct_stat statbuf; |
92 | if ( KDE_stat( QFile::encodeName( m_archiveName ), &statbuf ) == 0 ) |
93 | { |
94 | if ( m_mtime == statbuf.st_mtime ) |
95 | { |
96 | path = fullPath.mid( m_archiveName.length() ); |
97 | kDebug(7109) << "ArchiveProtocol::checkNewFile returning" << path; |
98 | return true; |
99 | } |
100 | } |
101 | } |
102 | kDebug(7109) << "Need to open a new file" ; |
103 | |
104 | // Close previous file |
105 | if ( m_archiveFile ) |
106 | { |
107 | m_archiveFile->close(); |
108 | delete m_archiveFile; |
109 | m_archiveFile = 0L; |
110 | } |
111 | |
112 | // Find where the tar file is in the full path |
113 | int pos = 0; |
114 | QString archiveFile; |
115 | path.clear(); |
116 | |
117 | int len = fullPath.length(); |
118 | if ( len != 0 && fullPath[ len - 1 ] != '/' ) |
119 | fullPath += '/'; |
120 | |
121 | kDebug(7109) << "the full path is" << fullPath; |
122 | KDE_struct_stat statbuf; |
123 | statbuf.st_mode = 0; // be sure to clear the directory bit |
124 | while ( (pos=fullPath.indexOf( '/', pos+1 )) != -1 ) |
125 | { |
126 | QString tryPath = fullPath.left( pos ); |
127 | kDebug(7109) << fullPath << "trying" << tryPath; |
128 | if ( KDE_stat( QFile::encodeName(tryPath), &statbuf ) == -1 ) |
129 | { |
130 | // We are not in the file system anymore, either we have already enough data or we will never get any useful data anymore |
131 | break; |
132 | } |
133 | if ( !S_ISDIR(statbuf.st_mode) ) |
134 | { |
135 | archiveFile = tryPath; |
136 | m_mtime = statbuf.st_mtime; |
137 | #ifdef Q_WS_WIN // st_uid and st_gid provides no information |
138 | m_user.clear(); |
139 | m_group.clear(); |
140 | #else |
141 | KUser user(statbuf.st_uid); |
142 | m_user = user.loginName(); |
143 | KUserGroup group(statbuf.st_gid); |
144 | m_group = group.name(); |
145 | #endif |
146 | path = fullPath.mid( pos + 1 ); |
147 | kDebug(7109).nospace() << "fullPath=" << fullPath << " path=" << path; |
148 | len = path.length(); |
149 | if ( len > 1 ) |
150 | { |
151 | if ( path[ len - 1 ] == '/' ) |
152 | path.truncate( len - 1 ); |
153 | } |
154 | else |
155 | path = QString::fromLatin1("/" ); |
156 | kDebug(7109).nospace() << "Found. archiveFile=" << archiveFile << " path=" << path; |
157 | break; |
158 | } |
159 | } |
160 | if ( archiveFile.isEmpty() ) |
161 | { |
162 | kDebug(7109) << "ArchiveProtocol::checkNewFile: not found" ; |
163 | if ( S_ISDIR(statbuf.st_mode) ) // Was the last stat about a directory? |
164 | { |
165 | // Too bad, it is a directory, not an archive. |
166 | kDebug(7109) << "Path is a directory, not an archive." ; |
167 | errorNum = KIO::ERR_IS_DIRECTORY; |
168 | } |
169 | else |
170 | errorNum = KIO::ERR_DOES_NOT_EXIST; |
171 | return false; |
172 | } |
173 | |
174 | // Open new file |
175 | if ( url.protocol() == "tar" ) { |
176 | kDebug(7109) << "Opening KTar on" << archiveFile; |
177 | m_archiveFile = new KTar( archiveFile ); |
178 | } else if ( url.protocol() == "ar" ) { |
179 | kDebug(7109) << "Opening KAr on " << archiveFile; |
180 | m_archiveFile = new KAr( archiveFile ); |
181 | } else if ( url.protocol() == "zip" ) { |
182 | kDebug(7109) << "Opening KZip on " << archiveFile; |
183 | m_archiveFile = new KZip( archiveFile ); |
184 | } else { |
185 | kWarning(7109) << "Protocol" << url.protocol() << "not supported by this IOSlave" ; |
186 | errorNum = KIO::ERR_UNSUPPORTED_PROTOCOL; |
187 | return false; |
188 | } |
189 | |
190 | if ( !m_archiveFile->open( QIODevice::ReadOnly ) ) |
191 | { |
192 | kDebug(7109) << "Opening" << archiveFile << "failed." ; |
193 | delete m_archiveFile; |
194 | m_archiveFile = 0L; |
195 | errorNum = KIO::ERR_CANNOT_OPEN_FOR_READING; |
196 | return false; |
197 | } |
198 | |
199 | m_archiveName = archiveFile; |
200 | return true; |
201 | } |
202 | |
203 | |
204 | void ArchiveProtocol::createRootUDSEntry( KIO::UDSEntry & entry ) |
205 | { |
206 | entry.clear(); |
207 | entry.insert( KIO::UDSEntry::UDS_NAME, "." ); |
208 | entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR ); |
209 | entry.insert( KIO::UDSEntry::UDS_MODIFICATION_TIME, m_mtime ); |
210 | //entry.insert( KIO::UDSEntry::UDS_ACCESS, 07777 ); // fake 'x' permissions, this is a pseudo-directory |
211 | entry.insert( KIO::UDSEntry::UDS_USER, m_user); |
212 | entry.insert( KIO::UDSEntry::UDS_GROUP, m_group); |
213 | } |
214 | |
215 | void ArchiveProtocol::createUDSEntry( const KArchiveEntry * archiveEntry, UDSEntry & entry ) |
216 | { |
217 | entry.clear(); |
218 | entry.insert( KIO::UDSEntry::UDS_NAME, archiveEntry->name() ); |
219 | entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, archiveEntry->permissions() & S_IFMT ); // keep file type only |
220 | entry.insert( KIO::UDSEntry::UDS_SIZE, archiveEntry->isFile() ? ((KArchiveFile *)archiveEntry)->size() : 0L ); |
221 | entry.insert( KIO::UDSEntry::UDS_MODIFICATION_TIME, archiveEntry->date()); |
222 | entry.insert( KIO::UDSEntry::UDS_ACCESS, archiveEntry->permissions() & 07777 ); // keep permissions only |
223 | entry.insert( KIO::UDSEntry::UDS_USER, archiveEntry->user()); |
224 | entry.insert( KIO::UDSEntry::UDS_GROUP, archiveEntry->group()); |
225 | entry.insert( KIO::UDSEntry::UDS_LINK_DEST, archiveEntry->symLinkTarget()); |
226 | } |
227 | |
228 | void ArchiveProtocol::listDir( const KUrl & url ) |
229 | { |
230 | kDebug( 7109 ) << "ArchiveProtocol::listDir" << url.url(); |
231 | |
232 | QString path; |
233 | KIO::Error errorNum; |
234 | if ( !checkNewFile( url, path, errorNum ) ) |
235 | { |
236 | if ( errorNum == KIO::ERR_CANNOT_OPEN_FOR_READING ) |
237 | { |
238 | // If we cannot open, it might be a problem with the archive header (e.g. unsupported format) |
239 | // Therefore give a more specific error message |
240 | error( KIO::ERR_SLAVE_DEFINED, |
241 | i18n( "Could not open the file, probably due to an unsupported file format.\n%1" , |
242 | url.prettyUrl() ) ); |
243 | return; |
244 | } |
245 | else if ( errorNum != ERR_IS_DIRECTORY ) |
246 | { |
247 | // We have any other error |
248 | error( errorNum, url.prettyUrl() ); |
249 | return; |
250 | } |
251 | // It's a real dir -> redirect |
252 | KUrl redir; |
253 | redir.setPath( url.path() ); |
254 | kDebug( 7109 ) << "Ok, redirection to" << redir.url(); |
255 | redirection( redir ); |
256 | finished(); |
257 | // And let go of the tar file - for people who want to unmount a cdrom after that |
258 | delete m_archiveFile; |
259 | m_archiveFile = 0L; |
260 | return; |
261 | } |
262 | |
263 | if ( path.isEmpty() ) |
264 | { |
265 | KUrl redir( url.protocol() + QString::fromLatin1( ":/" ) ); |
266 | kDebug( 7109 ) << "url.path()=" << url.path(); |
267 | redir.setPath( url.path() + QString::fromLatin1("/" ) ); |
268 | kDebug( 7109 ) << "ArchiveProtocol::listDir: redirection" << redir.url(); |
269 | redirection( redir ); |
270 | finished(); |
271 | return; |
272 | } |
273 | |
274 | kDebug( 7109 ) << "checkNewFile done" ; |
275 | const KArchiveDirectory* root = m_archiveFile->directory(); |
276 | const KArchiveDirectory* dir; |
277 | if (!path.isEmpty() && path != "/" ) |
278 | { |
279 | kDebug(7109) << "Looking for entry" << path; |
280 | const KArchiveEntry* e = root->entry( path ); |
281 | if ( !e ) |
282 | { |
283 | error( KIO::ERR_DOES_NOT_EXIST, url.prettyUrl() ); |
284 | return; |
285 | } |
286 | if ( ! e->isDirectory() ) |
287 | { |
288 | error( KIO::ERR_IS_FILE, url.prettyUrl() ); |
289 | return; |
290 | } |
291 | dir = (KArchiveDirectory*)e; |
292 | } else { |
293 | dir = root; |
294 | } |
295 | |
296 | const QStringList l = dir->entries(); |
297 | totalSize( l.count() ); |
298 | |
299 | UDSEntry entry; |
300 | if (!l.contains("." )) { |
301 | createRootUDSEntry(entry); |
302 | listEntry(entry, false); |
303 | } |
304 | |
305 | QStringList::const_iterator it = l.begin(); |
306 | for( ; it != l.end(); ++it ) |
307 | { |
308 | kDebug(7109) << (*it); |
309 | const KArchiveEntry* archiveEntry = dir->entry( (*it) ); |
310 | |
311 | createUDSEntry( archiveEntry, entry ); |
312 | |
313 | listEntry( entry, false ); |
314 | } |
315 | |
316 | listEntry( entry, true ); // ready |
317 | |
318 | finished(); |
319 | |
320 | kDebug( 7109 ) << "ArchiveProtocol::listDir done" ; |
321 | } |
322 | |
323 | void ArchiveProtocol::stat( const KUrl & url ) |
324 | { |
325 | QString path; |
326 | UDSEntry entry; |
327 | KIO::Error errorNum; |
328 | if ( !checkNewFile( url, path, errorNum ) ) |
329 | { |
330 | // We may be looking at a real directory - this happens |
331 | // when pressing up after being in the root of an archive |
332 | if ( errorNum == KIO::ERR_CANNOT_OPEN_FOR_READING ) |
333 | { |
334 | // If we cannot open, it might be a problem with the archive header (e.g. unsupported format) |
335 | // Therefore give a more specific error message |
336 | error( KIO::ERR_SLAVE_DEFINED, |
337 | i18n( "Could not open the file, probably due to an unsupported file format.\n%1" , |
338 | url.prettyUrl() ) ); |
339 | return; |
340 | } |
341 | else if ( errorNum != ERR_IS_DIRECTORY ) |
342 | { |
343 | // We have any other error |
344 | error( errorNum, url.prettyUrl() ); |
345 | return; |
346 | } |
347 | // Real directory. Return just enough information for KRun to work |
348 | entry.insert( KIO::UDSEntry::UDS_NAME, url.fileName()); |
349 | kDebug( 7109 ).nospace() << "ArchiveProtocol::stat returning name=" << url.fileName(); |
350 | |
351 | KDE_struct_stat buff; |
352 | #ifdef Q_WS_WIN |
353 | QString fullPath = url.path().remove(0, 1); |
354 | #else |
355 | QString fullPath = url.path(); |
356 | #endif |
357 | |
358 | if ( KDE_stat( QFile::encodeName( fullPath ), &buff ) == -1 ) |
359 | { |
360 | // Should not happen, as the file was already stated by checkNewFile |
361 | error( KIO::ERR_COULD_NOT_STAT, url.prettyUrl() ); |
362 | return; |
363 | } |
364 | |
365 | entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, buff.st_mode & S_IFMT); |
366 | |
367 | statEntry( entry ); |
368 | |
369 | finished(); |
370 | |
371 | // And let go of the tar file - for people who want to unmount a cdrom after that |
372 | delete m_archiveFile; |
373 | m_archiveFile = 0L; |
374 | return; |
375 | } |
376 | |
377 | const KArchiveDirectory* root = m_archiveFile->directory(); |
378 | const KArchiveEntry* archiveEntry; |
379 | if ( path.isEmpty() ) |
380 | { |
381 | path = QString::fromLatin1( "/" ); |
382 | archiveEntry = root; |
383 | } else { |
384 | archiveEntry = root->entry( path ); |
385 | } |
386 | if ( !archiveEntry ) |
387 | { |
388 | error( KIO::ERR_DOES_NOT_EXIST, url.prettyUrl() ); |
389 | return; |
390 | } |
391 | |
392 | createUDSEntry( archiveEntry, entry ); |
393 | statEntry( entry ); |
394 | |
395 | finished(); |
396 | } |
397 | |
398 | void ArchiveProtocol::get( const KUrl & url ) |
399 | { |
400 | kDebug( 7109 ) << "ArchiveProtocol::get" << url.url(); |
401 | |
402 | QString path; |
403 | KIO::Error errorNum; |
404 | if ( !checkNewFile( url, path, errorNum ) ) |
405 | { |
406 | if ( errorNum == KIO::ERR_CANNOT_OPEN_FOR_READING ) |
407 | { |
408 | // If we cannot open, it might be a problem with the archive header (e.g. unsupported format) |
409 | // Therefore give a more specific error message |
410 | error( KIO::ERR_SLAVE_DEFINED, |
411 | i18n( "Could not open the file, probably due to an unsupported file format.\n%1" , |
412 | url.prettyUrl() ) ); |
413 | return; |
414 | } |
415 | else |
416 | { |
417 | // We have any other error |
418 | error( errorNum, url.prettyUrl() ); |
419 | return; |
420 | } |
421 | } |
422 | |
423 | const KArchiveDirectory* root = m_archiveFile->directory(); |
424 | const KArchiveEntry* archiveEntry = root->entry( path ); |
425 | |
426 | if ( !archiveEntry ) |
427 | { |
428 | error( KIO::ERR_DOES_NOT_EXIST, url.prettyUrl() ); |
429 | return; |
430 | } |
431 | if ( archiveEntry->isDirectory() ) |
432 | { |
433 | error( KIO::ERR_IS_DIRECTORY, url.prettyUrl() ); |
434 | return; |
435 | } |
436 | const KArchiveFile* archiveFileEntry = static_cast<const KArchiveFile *>(archiveEntry); |
437 | if ( !archiveEntry->symLinkTarget().isEmpty() ) |
438 | { |
439 | kDebug(7109) << "Redirection to" << archiveEntry->symLinkTarget(); |
440 | KUrl realURL( url, archiveEntry->symLinkTarget() ); |
441 | kDebug(7109).nospace() << "realURL=" << realURL.url(); |
442 | redirection( realURL ); |
443 | finished(); |
444 | return; |
445 | } |
446 | |
447 | //kDebug(7109) << "Preparing to get the archive data"; |
448 | |
449 | /* |
450 | * The easy way would be to get the data by calling archiveFileEntry->data() |
451 | * However this has drawbacks: |
452 | * - the complete file must be read into the memory |
453 | * - errors are skipped, resulting in an empty file |
454 | */ |
455 | |
456 | QIODevice* io = archiveFileEntry->createDevice(); |
457 | |
458 | if (!io) |
459 | { |
460 | error( KIO::ERR_SLAVE_DEFINED, |
461 | i18n( "The archive file could not be opened, perhaps because the format is unsupported.\n%1" , |
462 | url.prettyUrl() ) ); |
463 | return; |
464 | } |
465 | |
466 | if ( !io->open( QIODevice::ReadOnly ) ) |
467 | { |
468 | error( KIO::ERR_CANNOT_OPEN_FOR_READING, url.prettyUrl() ); |
469 | delete io; |
470 | return; |
471 | } |
472 | |
473 | totalSize( archiveFileEntry->size() ); |
474 | |
475 | // Size of a QIODevice read. It must be large enough so that the mime type check will not fail |
476 | const qint64 maxSize = 0x100000; // 1MB |
477 | |
478 | qint64 bufferSize = qMin( maxSize, archiveFileEntry->size() ); |
479 | QByteArray buffer; |
480 | buffer.resize( bufferSize ); |
481 | if ( buffer.isEmpty() && bufferSize > 0 ) |
482 | { |
483 | // Something went wrong |
484 | error( KIO::ERR_OUT_OF_MEMORY, url.prettyUrl() ); |
485 | delete io; |
486 | return; |
487 | } |
488 | |
489 | bool firstRead = true; |
490 | |
491 | // How much file do we still have to process? |
492 | qint64 fileSize = archiveFileEntry->size(); |
493 | KIO::filesize_t processed = 0; |
494 | |
495 | while ( !io->atEnd() && fileSize > 0 ) |
496 | { |
497 | if ( !firstRead ) |
498 | { |
499 | bufferSize = qMin( maxSize, fileSize ); |
500 | buffer.resize( bufferSize ); |
501 | } |
502 | const qint64 read = io->read( buffer.data(), buffer.size() ); // Avoid to use bufferSize here, in case something went wrong. |
503 | if ( read != bufferSize ) |
504 | { |
505 | kWarning(7109) << "Read" << read << "bytes but expected" << bufferSize ; |
506 | error( KIO::ERR_COULD_NOT_READ, url.prettyUrl() ); |
507 | delete io; |
508 | return; |
509 | } |
510 | if ( firstRead ) |
511 | { |
512 | // We use the magic one the first data read |
513 | // (As magic detection is about fixed positions, we can be sure that it is enough data.) |
514 | KMimeType::Ptr mime = KMimeType::findByNameAndContent( path, buffer ); |
515 | kDebug(7109) << "Emitting mimetype" << mime->name(); |
516 | mimeType( mime->name() ); |
517 | firstRead = false; |
518 | } |
519 | data( buffer ); |
520 | processed += read; |
521 | processedSize( processed ); |
522 | fileSize -= bufferSize; |
523 | } |
524 | io->close(); |
525 | delete io; |
526 | |
527 | data( QByteArray() ); |
528 | |
529 | finished(); |
530 | } |
531 | |
532 | /* |
533 | In case someone wonders how the old filter stuff looked like : :) |
534 | void TARProtocol::slotData(void *_p, int _len) |
535 | { |
536 | switch (m_cmd) { |
537 | case CMD_PUT: |
538 | assert(m_pFilter); |
539 | m_pFilter->send(_p, _len); |
540 | break; |
541 | default: |
542 | abort(); |
543 | break; |
544 | } |
545 | } |
546 | |
547 | void TARProtocol::slotDataEnd() |
548 | { |
549 | switch (m_cmd) { |
550 | case CMD_PUT: |
551 | assert(m_pFilter && m_pJob); |
552 | m_pFilter->finish(); |
553 | m_pJob->dataEnd(); |
554 | m_cmd = CMD_NONE; |
555 | break; |
556 | default: |
557 | abort(); |
558 | break; |
559 | } |
560 | } |
561 | |
562 | void TARProtocol::jobData(void *_p, int _len) |
563 | { |
564 | switch (m_cmd) { |
565 | case CMD_GET: |
566 | assert(m_pFilter); |
567 | m_pFilter->send(_p, _len); |
568 | break; |
569 | case CMD_COPY: |
570 | assert(m_pFilter); |
571 | m_pFilter->send(_p, _len); |
572 | break; |
573 | default: |
574 | abort(); |
575 | } |
576 | } |
577 | |
578 | void TARProtocol::jobDataEnd() |
579 | { |
580 | switch (m_cmd) { |
581 | case CMD_GET: |
582 | assert(m_pFilter); |
583 | m_pFilter->finish(); |
584 | dataEnd(); |
585 | break; |
586 | case CMD_COPY: |
587 | assert(m_pFilter); |
588 | m_pFilter->finish(); |
589 | m_pJob->dataEnd(); |
590 | break; |
591 | default: |
592 | abort(); |
593 | } |
594 | } |
595 | |
596 | void TARProtocol::filterData(void *_p, int _len) |
597 | { |
598 | debug("void TARProtocol::filterData"); |
599 | switch (m_cmd) { |
600 | case CMD_GET: |
601 | data(_p, _len); |
602 | break; |
603 | case CMD_PUT: |
604 | assert (m_pJob); |
605 | m_pJob->data(_p, _len); |
606 | break; |
607 | case CMD_COPY: |
608 | assert(m_pJob); |
609 | m_pJob->data(_p, _len); |
610 | break; |
611 | default: |
612 | abort(); |
613 | } |
614 | } |
615 | */ |
616 | |
617 | // kate: space-indent on; indent-width 4; replace-tabs on; |
618 | |