mirror of
https://github.com/nzbget/nzbget.git
synced 2026-02-23 17:45:52 -05:00
1208 lines
28 KiB
C++
1208 lines
28 KiB
C++
/*
|
|
* This file is part of nzbget
|
|
*
|
|
* Copyright (C) 2004 Sven Henkel <sidddy@users.sourceforge.net>
|
|
* Copyright (C) 2007-2012 Andrey Prygunkov <hugbug@users.sourceforge.net>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* $Revision$
|
|
* $Date$
|
|
*
|
|
*/
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#ifdef WIN32
|
|
#include "win32.h"
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <cstdio>
|
|
#ifdef WIN32
|
|
#include <direct.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <sys/time.h>
|
|
#endif
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
|
|
#include "nzbget.h"
|
|
#include "ArticleDownloader.h"
|
|
#include "Decoder.h"
|
|
#include "Log.h"
|
|
#include "Options.h"
|
|
#include "ServerPool.h"
|
|
#include "Util.h"
|
|
|
|
extern DownloadSpeedMeter* g_pDownloadSpeedMeter;
|
|
extern DownloadQueueHolder* g_pDownloadQueueHolder;
|
|
extern Options* g_pOptions;
|
|
extern ServerPool* g_pServerPool;
|
|
|
|
ArticleDownloader::ArticleDownloader()
|
|
{
|
|
debug("Creating ArticleDownloader");
|
|
|
|
m_szResultFilename = NULL;
|
|
m_szTempFilename = NULL;
|
|
m_szArticleFilename = NULL;
|
|
m_szInfoName = NULL;
|
|
m_szOutputFilename = NULL;
|
|
m_pConnection = NULL;
|
|
m_eStatus = adUndefined;
|
|
m_bDuplicate = false;
|
|
m_eFormat = Decoder::efUnknown;
|
|
SetLastUpdateTimeNow();
|
|
}
|
|
|
|
ArticleDownloader::~ArticleDownloader()
|
|
{
|
|
debug("Destroying ArticleDownloader");
|
|
|
|
if (m_szTempFilename)
|
|
{
|
|
free(m_szTempFilename);
|
|
}
|
|
if (m_szArticleFilename)
|
|
{
|
|
free(m_szArticleFilename);
|
|
}
|
|
if (m_szInfoName)
|
|
{
|
|
free(m_szInfoName);
|
|
}
|
|
if (m_szOutputFilename)
|
|
{
|
|
free(m_szOutputFilename);
|
|
}
|
|
}
|
|
|
|
void ArticleDownloader::SetTempFilename(const char* v)
|
|
{
|
|
m_szTempFilename = strdup(v);
|
|
}
|
|
|
|
void ArticleDownloader::SetOutputFilename(const char* v)
|
|
{
|
|
m_szOutputFilename = strdup(v);
|
|
}
|
|
|
|
void ArticleDownloader::SetInfoName(const char * v)
|
|
{
|
|
m_szInfoName = strdup(v);
|
|
}
|
|
|
|
void ArticleDownloader::SetStatus(EStatus eStatus)
|
|
{
|
|
m_eStatus = eStatus;
|
|
Notify(NULL);
|
|
}
|
|
|
|
void ArticleDownloader::Run()
|
|
{
|
|
debug("Entering ArticleDownloader-loop");
|
|
|
|
SetStatus(adRunning);
|
|
m_szResultFilename = m_pArticleInfo->GetResultFilename();
|
|
|
|
if (g_pOptions->GetContinuePartial())
|
|
{
|
|
if (Util::FileExists(m_szResultFilename))
|
|
{
|
|
// file exists from previous program's start
|
|
detail("Article %s already downloaded, skipping", m_szInfoName);
|
|
SetStatus(adFinished);
|
|
FreeConnection(true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int iRemainedDownloadRetries = g_pOptions->GetRetries() > 0 ? g_pOptions->GetRetries() : 1;
|
|
|
|
#ifdef THREADCONNECT_WORKAROUND
|
|
// NOTE: about iRemainedConnectRetries:
|
|
// Sometimes connections just do not want to work in a particular thread,
|
|
// regardless of retry count. However they work in other threads.
|
|
// If ArticleDownloader can't start download after many attempts, it terminates
|
|
// and let QueueCoordinator retry the article in a new thread.
|
|
// It wasn't confirmed that this workaround actually helps.
|
|
// Therefore it is disabled by default. Define symbol "THREADCONNECT_WORKAROUND"
|
|
// to activate the workaround.
|
|
int iRemainedConnectRetries = iRemainedDownloadRetries > 5 ? iRemainedDownloadRetries * 2 : 10;
|
|
#endif
|
|
|
|
EStatus Status = adFailed;
|
|
int iMaxLevel = g_pServerPool->GetMaxLevel();
|
|
int* LevelStatus = (int*)malloc((iMaxLevel + 1) * sizeof(int));
|
|
for (int i = 0; i <= iMaxLevel; i++)
|
|
{
|
|
LevelStatus[i] = 0;
|
|
}
|
|
int level = 0;
|
|
|
|
while (!IsStopped() && iRemainedDownloadRetries > 0)
|
|
{
|
|
SetLastUpdateTimeNow();
|
|
|
|
Status = adFailed;
|
|
|
|
while (!IsStopped() && !m_pConnection)
|
|
{
|
|
m_pConnection = g_pServerPool->GetConnection(level);
|
|
usleep(5 * 1000);
|
|
}
|
|
|
|
if (IsStopped())
|
|
{
|
|
Status = adFailed;
|
|
break;
|
|
}
|
|
|
|
if (g_pOptions->GetPauseDownload() || g_pOptions->GetPauseDownload2())
|
|
{
|
|
Status = adRetry;
|
|
break;
|
|
}
|
|
|
|
m_pConnection->SetSuppressErrors(false);
|
|
|
|
// test connection
|
|
bool bConnected = m_pConnection && m_pConnection->Connect();
|
|
if (bConnected && !IsStopped())
|
|
{
|
|
// Okay, we got a Connection. Now start downloading.
|
|
detail("Downloading %s @ %s", m_szInfoName, m_pConnection->GetHost());
|
|
Status = Download();
|
|
}
|
|
|
|
if (bConnected)
|
|
{
|
|
if (Status == adConnectError)
|
|
{
|
|
m_pConnection->Disconnect();
|
|
bConnected = false;
|
|
Status = adFailed;
|
|
#ifdef THREADCONNECT_WORKAROUND
|
|
iRemainedConnectRetries--;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
// freeing connection allows other threads to start.
|
|
// we doing this only if the problem was with article or group.
|
|
// if the problem occurs by connecting or authorization we do not
|
|
// free the connection, to prevent starting of thousands of threads
|
|
// (cause each of them will also free it's connection after the
|
|
// same connect-error).
|
|
FreeConnection(Status == adFinished);
|
|
}
|
|
}
|
|
#ifdef THREADCONNECT_WORKAROUND
|
|
else
|
|
{
|
|
iRemainedConnectRetries--;
|
|
}
|
|
|
|
if (iRemainedConnectRetries == 0)
|
|
{
|
|
debug("Can't connect from this thread, retry later from another");
|
|
Status = adRetry;
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
if (((Status == adFailed) || (Status == adCrcError && g_pOptions->GetRetryOnCrcError())) &&
|
|
(iRemainedDownloadRetries > 1 || !bConnected) && !IsStopped() &&
|
|
!(g_pOptions->GetPauseDownload() || g_pOptions->GetPauseDownload2()))
|
|
{
|
|
detail("Waiting %i sec to retry", g_pOptions->GetRetryInterval());
|
|
int msec = 0;
|
|
while (!IsStopped() && (msec < g_pOptions->GetRetryInterval() * 1000) &&
|
|
!(g_pOptions->GetPauseDownload() || g_pOptions->GetPauseDownload2()))
|
|
{
|
|
usleep(100 * 1000);
|
|
msec += 100;
|
|
}
|
|
}
|
|
|
|
if (g_pOptions->GetPauseDownload() || g_pOptions->GetPauseDownload2())
|
|
{
|
|
Status = adRetry;
|
|
break;
|
|
}
|
|
|
|
if (IsStopped())
|
|
{
|
|
Status = adFailed;
|
|
break;
|
|
}
|
|
|
|
if ((Status == adFinished) || (Status == adFatalError) ||
|
|
(Status == adCrcError && !g_pOptions->GetRetryOnCrcError()))
|
|
{
|
|
break;
|
|
}
|
|
|
|
LevelStatus[level] = Status;
|
|
|
|
bool bAllLevelNotFound = true;
|
|
for (int lev = 0; lev <= iMaxLevel; lev++)
|
|
{
|
|
if (LevelStatus[lev] != adNotFound)
|
|
{
|
|
bAllLevelNotFound = false;
|
|
break;
|
|
}
|
|
}
|
|
if (bAllLevelNotFound)
|
|
{
|
|
if (iMaxLevel > 0)
|
|
{
|
|
warn("Article %s @ all servers failed: Article not found", m_szInfoName);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// do not count connect-errors, only article- and group-errors
|
|
if (bConnected)
|
|
{
|
|
level++;
|
|
if (level > iMaxLevel)
|
|
{
|
|
level = 0;
|
|
}
|
|
iRemainedDownloadRetries--;
|
|
}
|
|
}
|
|
|
|
FreeConnection(Status == adFinished);
|
|
|
|
free(LevelStatus);
|
|
|
|
if (m_bDuplicate)
|
|
{
|
|
Status = adFinished;
|
|
}
|
|
|
|
if (Status != adFinished && Status != adRetry)
|
|
{
|
|
Status = adFailed;
|
|
}
|
|
|
|
if (Status == adFailed)
|
|
{
|
|
if (IsStopped())
|
|
{
|
|
detail("Download %s cancelled", m_szInfoName);
|
|
}
|
|
else
|
|
{
|
|
warn("Download %s failed", m_szInfoName);
|
|
}
|
|
}
|
|
|
|
SetStatus(Status);
|
|
|
|
debug("Exiting ArticleDownloader-loop");
|
|
}
|
|
|
|
ArticleDownloader::EStatus ArticleDownloader::Download()
|
|
{
|
|
const char* szResponse = NULL;
|
|
EStatus Status = adRunning;
|
|
|
|
if (m_pConnection->GetNewsServer()->GetJoinGroup())
|
|
{
|
|
// change group
|
|
for (FileInfo::Groups::iterator it = m_pFileInfo->GetGroups()->begin(); it != m_pFileInfo->GetGroups()->end(); it++)
|
|
{
|
|
szResponse = m_pConnection->JoinGroup(*it);
|
|
if (szResponse && !strncmp(szResponse, "2", 1))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
Status = CheckResponse(szResponse, "could not join group");
|
|
if (Status != adFinished)
|
|
{
|
|
return Status;
|
|
}
|
|
}
|
|
|
|
// retrieve article
|
|
char tmp[1024];
|
|
snprintf(tmp, 1024, "ARTICLE %s\r\n", m_pArticleInfo->GetMessageID());
|
|
tmp[1024-1] = '\0';
|
|
|
|
for (int retry = 3; retry > 0; retry--)
|
|
{
|
|
szResponse = m_pConnection->Request(tmp);
|
|
if (szResponse && !strncmp(szResponse, "2", 1))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
Status = CheckResponse(szResponse, "could not fetch article");
|
|
if (Status != adFinished)
|
|
{
|
|
return Status;
|
|
}
|
|
|
|
// positive answer!
|
|
|
|
if (g_pOptions->GetDecode())
|
|
{
|
|
m_YDecoder.Clear();
|
|
m_YDecoder.SetAutoSeek(g_pOptions->GetDirectWrite());
|
|
m_YDecoder.SetCrcCheck(g_pOptions->GetCrcCheck());
|
|
|
|
m_UDecoder.Clear();
|
|
}
|
|
|
|
m_pOutFile = NULL;
|
|
bool bBody = false;
|
|
bool bEnd = false;
|
|
const int LineBufSize = 1024*10;
|
|
char* szLineBuf = (char*)malloc(LineBufSize);
|
|
Status = adRunning;
|
|
|
|
while (!IsStopped())
|
|
{
|
|
SetLastUpdateTimeNow();
|
|
|
|
// Throttle the bandwidth
|
|
while (!IsStopped() && (g_pOptions->GetDownloadRate() > 0.0f) &&
|
|
(g_pDownloadSpeedMeter->CalcCurrentDownloadSpeed() > g_pOptions->GetDownloadRate()))
|
|
{
|
|
SetLastUpdateTimeNow();
|
|
usleep(10 * 1000);
|
|
}
|
|
|
|
int iLen = 0;
|
|
char* line = m_pConnection->ReadLine(szLineBuf, LineBufSize, &iLen);
|
|
g_pDownloadSpeedMeter->AddSpeedReading(iLen);
|
|
|
|
// Have we encountered a timeout?
|
|
if (!line)
|
|
{
|
|
if (!IsStopped())
|
|
{
|
|
warn("Article %s @ %s failed: Unexpected end of article", m_szInfoName, m_pConnection->GetHost());
|
|
}
|
|
Status = adFailed;
|
|
break;
|
|
}
|
|
|
|
//detect end of article
|
|
if (!strcmp(line, ".\r\n") || !strcmp(line, ".\n"))
|
|
{
|
|
bEnd = true;
|
|
break;
|
|
}
|
|
|
|
//detect lines starting with "." (marked as "..")
|
|
if (!strncmp(line, "..", 2))
|
|
{
|
|
line++;
|
|
iLen--;
|
|
}
|
|
|
|
if (!bBody)
|
|
{
|
|
// detect body of article
|
|
if (*line == '\r' || *line == '\n')
|
|
{
|
|
bBody = true;
|
|
}
|
|
// check id of returned article
|
|
else if (!strncmp(line, "Message-ID: ", 12))
|
|
{
|
|
char* p = line + 12;
|
|
if (strncmp(p, m_pArticleInfo->GetMessageID(), strlen(m_pArticleInfo->GetMessageID())))
|
|
{
|
|
if (char* e = strrchr(p, '\r')) *e = '\0'; // remove trailing CR-character
|
|
warn("Article %s @ %s failed: Wrong message-id, expected %s, returned %s", m_szInfoName, m_pConnection->GetHost(), m_pArticleInfo->GetMessageID(), p);
|
|
Status = adFailed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (m_eFormat == Decoder::efUnknown && g_pOptions->GetDecode())
|
|
{
|
|
m_eFormat = Decoder::DetectFormat(line, iLen);
|
|
}
|
|
|
|
// write to output file
|
|
if (((bBody && m_eFormat != Decoder::efUnknown) || !g_pOptions->GetDecode()) && !Write(line, iLen))
|
|
{
|
|
Status = adFatalError;
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(szLineBuf);
|
|
|
|
if (m_pOutFile)
|
|
{
|
|
fclose(m_pOutFile);
|
|
}
|
|
|
|
if (!bEnd && Status == adRunning && !IsStopped())
|
|
{
|
|
warn("Article %s @ %s failed: article incomplete", m_szInfoName, m_pConnection->GetHost());
|
|
Status = adFailed;
|
|
}
|
|
|
|
if (IsStopped())
|
|
{
|
|
Status = adFailed;
|
|
}
|
|
|
|
if (Status == adRunning)
|
|
{
|
|
FreeConnection(true);
|
|
return DecodeCheck();
|
|
}
|
|
else
|
|
{
|
|
remove(m_szTempFilename);
|
|
return Status;
|
|
}
|
|
}
|
|
|
|
ArticleDownloader::EStatus ArticleDownloader::CheckResponse(const char* szResponse, const char* szComment)
|
|
{
|
|
if (!szResponse)
|
|
{
|
|
if (!IsStopped())
|
|
{
|
|
warn("Article %s @ %s failed, %s: Connection closed by remote host", m_szInfoName, m_pConnection->GetHost(), szComment);
|
|
}
|
|
return adConnectError;
|
|
}
|
|
else if (m_pConnection->GetAuthError() || !strncmp(szResponse, "400", 3) || !strncmp(szResponse, "499", 3))
|
|
{
|
|
warn("Article %s @ %s failed, %s: %s", m_szInfoName, m_pConnection->GetHost(), szComment, szResponse);
|
|
return adConnectError;
|
|
}
|
|
else if (!strncmp(szResponse, "41", 2) || !strncmp(szResponse, "42", 2) || !strncmp(szResponse, "43", 2))
|
|
{
|
|
warn("Article %s @ %s failed, %s: %s", m_szInfoName, m_pConnection->GetHost(), szComment, szResponse);
|
|
return adNotFound;
|
|
}
|
|
else if (!strncmp(szResponse, "2", 1))
|
|
{
|
|
// OK
|
|
return adFinished;
|
|
}
|
|
else
|
|
{
|
|
// unknown error, no special handling
|
|
warn("Article %s @ %s failed, %s: %s", m_szInfoName, m_pConnection->GetHost(), szComment, szResponse);
|
|
return adFailed;
|
|
}
|
|
}
|
|
|
|
bool ArticleDownloader::Write(char* szLine, int iLen)
|
|
{
|
|
if (!m_pOutFile && !PrepareFile(szLine))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (g_pOptions->GetDecode())
|
|
{
|
|
bool bOK = false;
|
|
if (m_eFormat == Decoder::efYenc)
|
|
{
|
|
bOK = m_YDecoder.Write(szLine, iLen, m_pOutFile);
|
|
}
|
|
else if (m_eFormat == Decoder::efUx)
|
|
{
|
|
bOK = m_UDecoder.Write(szLine, iLen, m_pOutFile);
|
|
}
|
|
else
|
|
{
|
|
warn("Decoding %s failed: unsupported encoding", m_szInfoName);
|
|
return false;
|
|
}
|
|
if (!bOK)
|
|
{
|
|
debug("Failed line: %s", szLine);
|
|
warn("Decoding %s failed", m_szInfoName);
|
|
}
|
|
return bOK;
|
|
}
|
|
else
|
|
{
|
|
return fwrite(szLine, 1, iLen, m_pOutFile) > 0;
|
|
}
|
|
}
|
|
|
|
bool ArticleDownloader::PrepareFile(char* szLine)
|
|
{
|
|
bool bOpen = false;
|
|
|
|
// prepare file for writing
|
|
if (m_eFormat == Decoder::efYenc)
|
|
{
|
|
if (!strncmp(szLine, "=ybegin ", 8))
|
|
{
|
|
if (g_pOptions->GetDupeCheck())
|
|
{
|
|
m_pFileInfo->LockOutputFile();
|
|
if (!m_pFileInfo->GetOutputInitialized())
|
|
{
|
|
char* pb = strstr(szLine, " name=");
|
|
if (pb)
|
|
{
|
|
pb += 6; //=strlen(" name=")
|
|
char* pe;
|
|
for (pe = pb; *pe != '\0' && *pe != '\n' && *pe != '\r'; pe++) ;
|
|
if (!m_szArticleFilename)
|
|
{
|
|
m_szArticleFilename = (char*)malloc(pe - pb + 1);
|
|
strncpy(m_szArticleFilename, pb, pe - pb);
|
|
m_szArticleFilename[pe - pb] = '\0';
|
|
}
|
|
if (m_pFileInfo->IsDupe(m_szArticleFilename))
|
|
{
|
|
m_bDuplicate = true;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (!g_pOptions->GetDirectWrite())
|
|
{
|
|
m_pFileInfo->SetOutputInitialized(true);
|
|
}
|
|
m_pFileInfo->UnlockOutputFile();
|
|
}
|
|
|
|
if (g_pOptions->GetDirectWrite())
|
|
{
|
|
char* pb = strstr(szLine, " size=");
|
|
if (pb)
|
|
{
|
|
m_pFileInfo->LockOutputFile();
|
|
if (!m_pFileInfo->GetOutputInitialized())
|
|
{
|
|
pb += 6; //=strlen(" size=")
|
|
long iArticleFilesize = atol(pb);
|
|
if (!CreateOutputFile(iArticleFilesize))
|
|
{
|
|
return false;
|
|
}
|
|
m_pFileInfo->SetOutputInitialized(true);
|
|
}
|
|
m_pFileInfo->UnlockOutputFile();
|
|
bOpen = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bOpen = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bOpen = true;
|
|
}
|
|
|
|
if (bOpen)
|
|
{
|
|
bool bDirectWrite = g_pOptions->GetDirectWrite() && m_eFormat == Decoder::efYenc;
|
|
const char* szFilename = bDirectWrite ? m_szOutputFilename : m_szTempFilename;
|
|
m_pOutFile = fopen(szFilename, bDirectWrite ? "rb+" : "wb");
|
|
if (!m_pOutFile)
|
|
{
|
|
error("Could not %s file %s", bDirectWrite ? "open" : "create", szFilename);
|
|
return false;
|
|
}
|
|
if (g_pOptions->GetWriteBufferSize() == -1)
|
|
{
|
|
setvbuf(m_pOutFile, (char *)NULL, _IOFBF, m_pArticleInfo->GetSize());
|
|
}
|
|
else if (g_pOptions->GetWriteBufferSize() > 0)
|
|
{
|
|
setvbuf(m_pOutFile, (char *)NULL, _IOFBF, g_pOptions->GetWriteBufferSize());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* creates output file and subdirectores */
|
|
bool ArticleDownloader::CreateOutputFile(int iSize)
|
|
{
|
|
// delete eventually existing old file from previous program session
|
|
remove(m_szOutputFilename);
|
|
|
|
// ensure the directory exist
|
|
char szDestDir[1024];
|
|
int iMaxlen = Util::BaseFileName(m_szOutputFilename) - m_szOutputFilename;
|
|
strncpy(szDestDir, m_szOutputFilename, iMaxlen < 1024 ? iMaxlen : 1024-1);
|
|
szDestDir[iMaxlen] = '\0';
|
|
|
|
if (!Util::ForceDirectories(szDestDir))
|
|
{
|
|
error("Could not create directory %s! Errcode: %i", szDestDir, errno);
|
|
return false;
|
|
}
|
|
|
|
if (!Util::CreateSparseFile(m_szOutputFilename, iSize))
|
|
{
|
|
error("Could not create file %s!", m_szOutputFilename);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ArticleDownloader::EStatus ArticleDownloader::DecodeCheck()
|
|
{
|
|
bool bDirectWrite = g_pOptions->GetDirectWrite() && m_eFormat == Decoder::efYenc;
|
|
|
|
if (g_pOptions->GetDecode())
|
|
{
|
|
SetStatus(adDecoding);
|
|
|
|
Decoder* pDecoder = NULL;
|
|
|
|
if (m_eFormat == Decoder::efYenc)
|
|
{
|
|
pDecoder = &m_YDecoder;
|
|
}
|
|
else if (m_eFormat == Decoder::efUx)
|
|
{
|
|
pDecoder = &m_UDecoder;
|
|
}
|
|
else
|
|
{
|
|
warn("Decoding %s failed: no binary data or unsupported encoding format", m_szInfoName);
|
|
return adFatalError;
|
|
}
|
|
|
|
Decoder::EStatus eStatus = pDecoder->Check();
|
|
bool bOK = eStatus == Decoder::eFinished;
|
|
|
|
if (!bDirectWrite && bOK)
|
|
{
|
|
if (!Util::MoveFile(m_szTempFilename, m_szResultFilename))
|
|
{
|
|
error("Could not rename file %s to %s! Errcode: %i", m_szTempFilename, m_szResultFilename, errno);
|
|
}
|
|
}
|
|
|
|
if (!m_szArticleFilename && pDecoder->GetArticleFilename())
|
|
{
|
|
m_szArticleFilename = strdup(pDecoder->GetArticleFilename());
|
|
}
|
|
|
|
remove(m_szTempFilename);
|
|
|
|
if (bOK)
|
|
{
|
|
detail("Successfully downloaded %s", m_szInfoName);
|
|
|
|
if (bDirectWrite && g_pOptions->GetContinuePartial())
|
|
{
|
|
// create empty flag-file to indicate that the artcile was downloaded
|
|
FILE* flagfile = fopen(m_szResultFilename, "wb");
|
|
if (!flagfile)
|
|
{
|
|
error("Could not create file %s", m_szResultFilename);
|
|
// this error can be ignored
|
|
}
|
|
fclose(flagfile);
|
|
}
|
|
|
|
return adFinished;
|
|
}
|
|
else
|
|
{
|
|
remove(m_szResultFilename);
|
|
if (eStatus == Decoder::eCrcError)
|
|
{
|
|
warn("Decoding %s failed: CRC-Error", m_szInfoName);
|
|
return adCrcError;
|
|
}
|
|
else if (eStatus == Decoder::eArticleIncomplete)
|
|
{
|
|
warn("Decoding %s failed: article incomplete", m_szInfoName);
|
|
return adFailed;
|
|
}
|
|
else if (eStatus == Decoder::eInvalidSize)
|
|
{
|
|
warn("Decoding %s failed: size mismatch", m_szInfoName);
|
|
return adFailed;
|
|
}
|
|
else if (eStatus == Decoder::eNoBinaryData)
|
|
{
|
|
warn("Decoding %s failed: no binary data found", m_szInfoName);
|
|
return adFailed;
|
|
}
|
|
else
|
|
{
|
|
warn("Decoding %s failed", m_szInfoName);
|
|
return adFailed;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// rawmode
|
|
if (Util::MoveFile(m_szTempFilename, m_szResultFilename))
|
|
{
|
|
detail("Article %s successfully downloaded", m_szInfoName);
|
|
}
|
|
else
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", m_szTempFilename, m_szResultFilename, errno);
|
|
}
|
|
return adFinished;
|
|
}
|
|
}
|
|
|
|
void ArticleDownloader::LogDebugInfo()
|
|
{
|
|
char szTime[50];
|
|
#ifdef HAVE_CTIME_R_3
|
|
ctime_r(&m_tLastUpdateTime, szTime, 50);
|
|
#else
|
|
ctime_r(&m_tLastUpdateTime, szTime);
|
|
#endif
|
|
|
|
debug(" Download: status=%i, LastUpdateTime=%s, filename=%s", m_eStatus, szTime, Util::BaseFileName(GetTempFilename()));
|
|
}
|
|
|
|
void ArticleDownloader::Stop()
|
|
{
|
|
debug("Trying to stop ArticleDownloader");
|
|
Thread::Stop();
|
|
m_mutexConnection.Lock();
|
|
if (m_pConnection)
|
|
{
|
|
m_pConnection->SetSuppressErrors(true);
|
|
m_pConnection->Cancel();
|
|
}
|
|
m_mutexConnection.Unlock();
|
|
debug("ArticleDownloader stopped successfully");
|
|
}
|
|
|
|
bool ArticleDownloader::Terminate()
|
|
{
|
|
NNTPConnection* pConnection = m_pConnection;
|
|
bool terminated = Kill();
|
|
if (terminated && pConnection)
|
|
{
|
|
debug("Terminating connection");
|
|
pConnection->SetSuppressErrors(true);
|
|
pConnection->Cancel();
|
|
pConnection->Disconnect();
|
|
g_pServerPool->FreeConnection(pConnection, true);
|
|
}
|
|
return terminated;
|
|
}
|
|
|
|
void ArticleDownloader::FreeConnection(bool bKeepConnected)
|
|
{
|
|
if (m_pConnection)
|
|
{
|
|
debug("Releasing connection");
|
|
m_mutexConnection.Lock();
|
|
if (!bKeepConnected || m_pConnection->GetStatus() == Connection::csCancelled)
|
|
{
|
|
m_pConnection->Disconnect();
|
|
}
|
|
g_pServerPool->FreeConnection(m_pConnection, true);
|
|
m_pConnection = NULL;
|
|
m_mutexConnection.Unlock();
|
|
}
|
|
}
|
|
|
|
void ArticleDownloader::CompleteFileParts()
|
|
{
|
|
debug("Completing file parts");
|
|
debug("ArticleFilename: %s", m_pFileInfo->GetFilename());
|
|
|
|
SetStatus(adJoining);
|
|
|
|
bool bDirectWrite = g_pOptions->GetDirectWrite() && m_pFileInfo->GetOutputInitialized();
|
|
|
|
char szNZBName[1024];
|
|
char szNZBDestDir[1024];
|
|
// the locking is needed for accessing the memebers of NZBInfo
|
|
g_pDownloadQueueHolder->LockQueue();
|
|
strncpy(szNZBName, m_pFileInfo->GetNZBInfo()->GetName(), 1024);
|
|
strncpy(szNZBDestDir, m_pFileInfo->GetNZBInfo()->GetDestDir(), 1024);
|
|
g_pDownloadQueueHolder->UnlockQueue();
|
|
szNZBName[1024-1] = '\0';
|
|
szNZBDestDir[1024-1] = '\0';
|
|
|
|
char InfoFilename[1024];
|
|
snprintf(InfoFilename, 1024, "%s%c%s", szNZBName, (int)PATH_SEPARATOR, m_pFileInfo->GetFilename());
|
|
InfoFilename[1024-1] = '\0';
|
|
|
|
if (!g_pOptions->GetDecode())
|
|
{
|
|
detail("Moving articles for %s", InfoFilename);
|
|
}
|
|
else if (bDirectWrite)
|
|
{
|
|
detail("Checking articles for %s", InfoFilename);
|
|
}
|
|
else
|
|
{
|
|
detail("Joining articles for %s", InfoFilename);
|
|
}
|
|
|
|
// Ensure the DstDir is created
|
|
if (!Util::ForceDirectories(szNZBDestDir))
|
|
{
|
|
error("Could not create directory %s! Errcode: %i", szNZBDestDir, errno);
|
|
SetStatus(adJoined);
|
|
return;
|
|
}
|
|
|
|
char ofn[1024];
|
|
snprintf(ofn, 1024, "%s%c%s", szNZBDestDir, (int)PATH_SEPARATOR, m_pFileInfo->GetFilename());
|
|
ofn[1024-1] = '\0';
|
|
|
|
// prevent overwriting existing files
|
|
int dupcount = 0;
|
|
while (Util::FileExists(ofn))
|
|
{
|
|
dupcount++;
|
|
snprintf(ofn, 1024, "%s%c%s_duplicate%d", szNZBDestDir, (int)PATH_SEPARATOR, m_pFileInfo->GetFilename(), dupcount);
|
|
ofn[1024-1] = '\0';
|
|
}
|
|
|
|
FILE* outfile = NULL;
|
|
char tmpdestfile[1024];
|
|
snprintf(tmpdestfile, 1024, "%s.tmp", ofn);
|
|
tmpdestfile[1024-1] = '\0';
|
|
|
|
if (g_pOptions->GetDecode() && !bDirectWrite)
|
|
{
|
|
remove(tmpdestfile);
|
|
outfile = fopen(tmpdestfile, "wb+");
|
|
if (!outfile)
|
|
{
|
|
error("Could not create file %s!", tmpdestfile);
|
|
SetStatus(adJoined);
|
|
return;
|
|
}
|
|
if (g_pOptions->GetWriteBufferSize() == -1 && (*m_pFileInfo->GetArticles())[0])
|
|
{
|
|
setvbuf(outfile, (char *)NULL, _IOFBF, (*m_pFileInfo->GetArticles())[0]->GetSize());
|
|
}
|
|
else if (g_pOptions->GetWriteBufferSize() > 0)
|
|
{
|
|
setvbuf(outfile, (char *)NULL, _IOFBF, g_pOptions->GetWriteBufferSize());
|
|
}
|
|
}
|
|
else if (!g_pOptions->GetDecode())
|
|
{
|
|
remove(tmpdestfile);
|
|
if (!Util::CreateDirectory(ofn))
|
|
{
|
|
error("Could not create directory %s! Errcode: %i", ofn, errno);
|
|
SetStatus(adJoined);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool complete = true;
|
|
int iBrokenCount = 0;
|
|
static const int BUFFER_SIZE = 1024 * 50;
|
|
char* buffer = NULL;
|
|
|
|
if (g_pOptions->GetDecode() && !bDirectWrite)
|
|
{
|
|
buffer = (char*)malloc(BUFFER_SIZE);
|
|
}
|
|
|
|
for (FileInfo::Articles::iterator it = m_pFileInfo->GetArticles()->begin(); it != m_pFileInfo->GetArticles()->end(); it++)
|
|
{
|
|
ArticleInfo* pa = *it;
|
|
if (pa->GetStatus() != ArticleInfo::aiFinished)
|
|
{
|
|
iBrokenCount++;
|
|
complete = false;
|
|
}
|
|
else if (g_pOptions->GetDecode() && !bDirectWrite)
|
|
{
|
|
FILE* infile;
|
|
const char* fn = pa->GetResultFilename();
|
|
|
|
infile = fopen(fn, "rb");
|
|
if (infile)
|
|
{
|
|
int cnt = BUFFER_SIZE;
|
|
|
|
while (cnt == BUFFER_SIZE)
|
|
{
|
|
cnt = (int)fread(buffer, 1, BUFFER_SIZE, infile);
|
|
fwrite(buffer, 1, cnt, outfile);
|
|
SetLastUpdateTimeNow();
|
|
}
|
|
|
|
fclose(infile);
|
|
}
|
|
else
|
|
{
|
|
complete = false;
|
|
iBrokenCount++;
|
|
detail("Could not find file %s. Status is broken", fn);
|
|
}
|
|
}
|
|
else if (!g_pOptions->GetDecode())
|
|
{
|
|
const char* fn = pa->GetResultFilename();
|
|
char dstFileName[1024];
|
|
snprintf(dstFileName, 1024, "%s%c%03i", ofn, (int)PATH_SEPARATOR, pa->GetPartNumber());
|
|
dstFileName[1024-1] = '\0';
|
|
if (!Util::MoveFile(fn, dstFileName))
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", fn, dstFileName, errno);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buffer)
|
|
{
|
|
free(buffer);
|
|
}
|
|
|
|
if (outfile)
|
|
{
|
|
fclose(outfile);
|
|
if (!Util::MoveFile(tmpdestfile, ofn))
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", tmpdestfile, ofn, errno);
|
|
}
|
|
}
|
|
|
|
if (bDirectWrite)
|
|
{
|
|
if (!Util::MoveFile(m_szOutputFilename, ofn))
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", m_szOutputFilename, ofn, errno);
|
|
}
|
|
|
|
// if destination directory was changed delete the old directory (if empty)
|
|
int iLen = strlen(szNZBDestDir);
|
|
if (!(!strncmp(szNZBDestDir, m_szOutputFilename, iLen) &&
|
|
(m_szOutputFilename[iLen] == PATH_SEPARATOR || m_szOutputFilename[iLen] == ALT_PATH_SEPARATOR)))
|
|
{
|
|
debug("Checking old dir for: %s", m_szOutputFilename);
|
|
char szOldDestDir[1024];
|
|
int iMaxlen = Util::BaseFileName(m_szOutputFilename) - m_szOutputFilename;
|
|
strncpy(szOldDestDir, m_szOutputFilename, iMaxlen < 1024 ? iMaxlen : 1024-1);
|
|
szOldDestDir[iMaxlen] = '\0';
|
|
if (Util::DirEmpty(szOldDestDir))
|
|
{
|
|
debug("Deleting old dir: %s", szOldDestDir);
|
|
rmdir(szOldDestDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bDirectWrite || g_pOptions->GetContinuePartial())
|
|
{
|
|
for (FileInfo::Articles::iterator it = m_pFileInfo->GetArticles()->begin(); it != m_pFileInfo->GetArticles()->end(); it++)
|
|
{
|
|
ArticleInfo* pa = *it;
|
|
remove(pa->GetResultFilename());
|
|
}
|
|
}
|
|
|
|
if (complete)
|
|
{
|
|
info("Successfully downloaded %s", InfoFilename);
|
|
}
|
|
else
|
|
{
|
|
warn("%i of %i article downloads failed for \"%s\"", iBrokenCount, m_pFileInfo->GetArticles()->size(), InfoFilename);
|
|
|
|
if (g_pOptions->GetRenameBroken())
|
|
{
|
|
char brokenfn[1024];
|
|
snprintf(brokenfn, 1024, "%s_broken", ofn);
|
|
brokenfn[1024-1] = '\0';
|
|
if (Util::MoveFile(ofn, brokenfn))
|
|
{
|
|
detail("Renaming broken file from %s to %s", ofn, brokenfn);
|
|
}
|
|
else
|
|
{
|
|
warn("Renaming broken file from %s to %s failed", ofn, brokenfn);
|
|
}
|
|
strncpy(ofn, brokenfn, 1024);
|
|
ofn[1024-1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
detail("Not renaming broken file %s", ofn);
|
|
}
|
|
|
|
if (g_pOptions->GetCreateBrokenLog())
|
|
{
|
|
char szBrokenLogName[1024];
|
|
snprintf(szBrokenLogName, 1024, "%s%c_brokenlog.txt", szNZBDestDir, (int)PATH_SEPARATOR);
|
|
szBrokenLogName[1024-1] = '\0';
|
|
FILE* file = fopen(szBrokenLogName, "ab");
|
|
fprintf(file, "%s (%i/%i)%s", m_pFileInfo->GetFilename(), m_pFileInfo->GetArticles()->size() - iBrokenCount, m_pFileInfo->GetArticles()->size(), LINE_ENDING);
|
|
fclose(file);
|
|
}
|
|
}
|
|
|
|
// the locking is needed for accessing the memebers of NZBInfo
|
|
g_pDownloadQueueHolder->LockQueue();
|
|
m_pFileInfo->GetNZBInfo()->GetCompletedFiles()->push_back(strdup(ofn));
|
|
if (strcmp(m_pFileInfo->GetNZBInfo()->GetDestDir(), szNZBDestDir))
|
|
{
|
|
// destination directory was changed during completion, need to move the file
|
|
MoveCompletedFiles(m_pFileInfo->GetNZBInfo(), szNZBDestDir);
|
|
}
|
|
g_pDownloadQueueHolder->UnlockQueue();
|
|
|
|
SetStatus(adJoined);
|
|
}
|
|
|
|
bool ArticleDownloader::MoveCompletedFiles(NZBInfo* pNZBInfo, const char* szOldDestDir)
|
|
{
|
|
if (pNZBInfo->GetCompletedFiles()->empty())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Ensure the DstDir is created
|
|
if (!Util::ForceDirectories(pNZBInfo->GetDestDir()))
|
|
{
|
|
error("Could not create directory %s! Errcode: %i", pNZBInfo->GetDestDir(), errno);
|
|
return false;
|
|
}
|
|
|
|
// move already downloaded files to new destination
|
|
for (NZBInfo::Files::iterator it = pNZBInfo->GetCompletedFiles()->begin(); it != pNZBInfo->GetCompletedFiles()->end(); it++)
|
|
{
|
|
char* szFileName = *it;
|
|
char szNewFileName[1024];
|
|
snprintf(szNewFileName, 1024, "%s%c%s", pNZBInfo->GetDestDir(), (int)PATH_SEPARATOR, Util::BaseFileName(szFileName));
|
|
szNewFileName[1024-1] = '\0';
|
|
|
|
// check if file was not moved already
|
|
if (strcmp(szFileName, szNewFileName))
|
|
{
|
|
// prevent overwriting of existing files
|
|
int dupcount = 0;
|
|
while (Util::FileExists(szNewFileName))
|
|
{
|
|
dupcount++;
|
|
snprintf(szNewFileName, 1024, "%s%c%s_duplicate%d", pNZBInfo->GetDestDir(), (int)PATH_SEPARATOR, Util::BaseFileName(szFileName), dupcount);
|
|
szNewFileName[1024-1] = '\0';
|
|
}
|
|
|
|
detail("Moving file %s to %s", szFileName, szNewFileName);
|
|
if (Util::MoveFile(szFileName, szNewFileName))
|
|
{
|
|
free(szFileName);
|
|
*it = strdup(szNewFileName);
|
|
}
|
|
else
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", szFileName, szNewFileName, errno);
|
|
}
|
|
}
|
|
}
|
|
|
|
// move brokenlog.txt
|
|
if (g_pOptions->GetCreateBrokenLog())
|
|
{
|
|
char szOldBrokenLogName[1024];
|
|
snprintf(szOldBrokenLogName, 1024, "%s%c_brokenlog.txt", szOldDestDir, (int)PATH_SEPARATOR);
|
|
szOldBrokenLogName[1024-1] = '\0';
|
|
if (Util::FileExists(szOldBrokenLogName))
|
|
{
|
|
char szBrokenLogName[1024];
|
|
snprintf(szBrokenLogName, 1024, "%s%c_brokenlog.txt", pNZBInfo->GetDestDir(), (int)PATH_SEPARATOR);
|
|
szBrokenLogName[1024-1] = '\0';
|
|
|
|
detail("Moving file %s to %s", szOldBrokenLogName, szBrokenLogName);
|
|
if (Util::FileExists(szBrokenLogName))
|
|
{
|
|
// copy content to existing new file, then delete old file
|
|
FILE* outfile;
|
|
outfile = fopen(szBrokenLogName, "ab");
|
|
if (outfile)
|
|
{
|
|
FILE* infile;
|
|
infile = fopen(szOldBrokenLogName, "rb");
|
|
if (infile)
|
|
{
|
|
static const int BUFFER_SIZE = 1024 * 50;
|
|
int cnt = BUFFER_SIZE;
|
|
char* buffer = (char*)malloc(BUFFER_SIZE);
|
|
while (cnt == BUFFER_SIZE)
|
|
{
|
|
cnt = (int)fread(buffer, 1, BUFFER_SIZE, infile);
|
|
fwrite(buffer, 1, cnt, outfile);
|
|
}
|
|
fclose(infile);
|
|
free(buffer);
|
|
remove(szOldBrokenLogName);
|
|
}
|
|
else
|
|
{
|
|
error("Could not open file %s", szOldBrokenLogName);
|
|
}
|
|
fclose(outfile);
|
|
}
|
|
else
|
|
{
|
|
error("Could not open file %s", szBrokenLogName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// move to new destination
|
|
if (!Util::MoveFile(szOldBrokenLogName, szBrokenLogName))
|
|
{
|
|
error("Could not move file %s to %s! Errcode: %i", szOldBrokenLogName, szBrokenLogName, errno);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete old directory (if empty)
|
|
if (Util::DirEmpty(szOldDestDir))
|
|
{
|
|
rmdir(szOldDestDir);
|
|
}
|
|
|
|
return true;
|
|
}
|